diff --git a/src/components/ExpandIcon.tsx b/src/components/ExpandIcon.tsx index cb9902b..17c708a 100644 --- a/src/components/ExpandIcon.tsx +++ b/src/components/ExpandIcon.tsx @@ -1,5 +1,4 @@ import classNames from "classnames"; -import { ReactNode } from "react"; import { ChevronRight } from "src/assets/icons"; @@ -8,22 +7,15 @@ type Props = { hideIcon: boolean; }; const ExpandIcon = ({ isExpanded, hideIcon }: Props) => { - let content: ReactNode; - if (hideIcon) { - content = null; - } else { - content = ( - - ); - } - return (
- {content} + {hideIcon ? null : ( + + )}
); }; diff --git a/src/components/Explorer/DragAndDrop.tsx b/src/components/Explorer/DragAndDrop.tsx index a2568d1..e2a7375 100644 --- a/src/components/Explorer/DragAndDrop.tsx +++ b/src/components/Explorer/DragAndDrop.tsx @@ -29,16 +29,16 @@ const DragAndDropExplorer = () => { moveFile, moveFolder, expandFolder, - setFocusedFolder, - selectFile, + changeFocusedFolder, + selectFileAndOpen, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ expandedFolderPaths: store.expandedFolderPaths, moveFile: store.moveFile, moveFolder: store.moveFolder, expandFolder: store.expandFolder, - setFocusedFolder: store.setFocusedFolder, - selectFile: store.selectFile, + changeFocusedFolder: store.changeFocusedFolder, + selectFileAndOpen: store.selectFileAndOpen, })) ); @@ -77,8 +77,8 @@ const DragAndDropExplorer = () => { const newPath = targetFolder.path + "/" + file.name; await moveFile(file, newPath); if (openDestinationFolder) { - await setFocusedFolder(targetFolder); - await selectFile(file); + await changeFocusedFolder(targetFolder); + await selectFileAndOpen(file); } }; @@ -89,7 +89,7 @@ const DragAndDropExplorer = () => { const newPath = targetFolder.path + "/" + folder.name; await moveFolder(folder, newPath); if (openDestinationFolder) { - await setFocusedFolder(targetFolder); + await changeFocusedFolder(targetFolder); } }; diff --git a/src/components/File/Content.tsx b/src/components/File/Content.tsx index be3891c..edb6345 100644 --- a/src/components/File/Content.tsx +++ b/src/components/File/Content.tsx @@ -27,7 +27,7 @@ const FileContent = ({ file }: FileProps) => { const { focusedFile, - selectFile, + selectFileAndOpen, createFile, duplicateFile, isFilePinned, @@ -38,7 +38,7 @@ const FileContent = ({ file }: FileProps) => { } = useExplorerStore( useShallow((store: ExplorerStore) => ({ focusedFile: store.focusedFile, - selectFile: store.selectFile, + selectFileAndOpen: store.selectFileAndOpen, createFile: store.createFile, duplicateFile: store.duplicateFile, isFilePinned: store.isFilePinned, @@ -212,7 +212,7 @@ const FileContent = ({ file }: FileProps) => { const openFileInNewTab = (file: TFile) => { plugin.app.workspace.openLinkText(file.path, file.path, true); - selectFile(file); + selectFileAndOpen(file); }; const maybeRenderFileDetail = () => { diff --git a/src/components/File/index.tsx b/src/components/File/index.tsx index 72a343b..04d35ab 100644 --- a/src/components/File/index.tsx +++ b/src/components/File/index.tsx @@ -17,9 +17,9 @@ const File = ({ file, disableDrag, onOpenFoldersPane }: Props) => { const { useExplorerStore, plugin } = useExplorer(); const { language } = plugin; - const { selectFile } = useExplorerStore( + const { selectFileAndOpen } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - selectFile: store.selectFile, + selectFileAndOpen: store.selectFileAndOpen, })) ); @@ -55,7 +55,7 @@ const File = ({ file, disableDrag, onOpenFoldersPane }: Props) => { className="ffs__file-tree-item tree-item nav-file" ref={setNodeRef} style={{ opacity: isDragging ? 0.5 : 1 }} - onClick={() => selectFile(file)} + onClick={() => selectFileAndOpen(file)} data-tooltip-position="right" aria-label={getAriaLabel()} {...attributes} diff --git a/src/components/FileActions/SortFiles.tsx b/src/components/FileActions/SortFiles.tsx index c818df2..f74747c 100644 --- a/src/components/FileActions/SortFiles.tsx +++ b/src/components/FileActions/SortFiles.tsx @@ -1,7 +1,7 @@ import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import { FILE_SORT_RULES_COPY } from "src/locales"; +import { FILE_SORT_RULES_COPY, SORT_TIPS_COPY } from "src/locales"; import { ExplorerStore } from "src/store"; import { FILE_MANUAL_SORT_RULE } from "src/store/file/sort"; import { FileSortRule } from "src/store/file/sort"; @@ -9,114 +9,54 @@ import { FileSortRule } from "src/store/file/sort"; import { ManualSortFilesModal } from "../ManualSortFilesModal"; import SortAction from "../SortAction"; - -type FileSortRuleItem = { - text: string; - rule: FileSortRule; -}; -type FileSortRuleGroup = FileSortRuleItem[]; - const SortFiles = () => { const { useExplorerStore, plugin } = useExplorer(); const { language } = plugin; const { fileSortRule, - changeFileSortRule, - isFilesInAscendingOrder, - initFilesManualSortOrder, focusedFolder, + filesSortRulesGroup, + changeFileSortRuleAndUpdateOrder, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ fileSortRule: store.fileSortRule, - isFilesInAscendingOrder: store.isFilesInAscendingOrder, - changeFileSortRule: store.changeFileSortRule, - initFilesManualSortOrder: store.initFilesManualSortOrder, focusedFolder: store.focusedFolder, + filesSortRulesGroup: store.filesSortRulesGroup, + changeFileSortRuleAndUpdateOrder: + store.changeFileSortRuleAndUpdateOrder, })) ); - const getRuleGroups = () => { - const { - fileNameAscending, - fileNameDescending, - modifiledTimeAscending, - modifiledTimeDescending, - createdTimeAscending, - createdTimeDescending, - manualOrder, - } = FILE_SORT_RULES_COPY; - const fileSortByNameRules: FileSortRuleGroup = [ - { text: fileNameAscending[language], rule: "FileNameAscending" }, - { text: fileNameDescending[language], rule: "FileNameDescending" }, - ]; - const fileSortByModifiedTimeRules: FileSortRuleGroup = [ - { - text: modifiledTimeDescending[language], - rule: "FileModifiedTimeDescending", - }, - { - text: modifiledTimeAscending[language], - rule: "FileModifiedTimeAscending", - }, - ]; - const fileSortByCreatedTimeRules: FileSortRuleGroup = [ - { - text: createdTimeDescending[language], - rule: "FileCreatedTimeDescending", - }, - { - text: createdTimeAscending[language], - rule: "FileCreatedTimeAscending", - }, - ]; - const filesManualSortRules: FileSortRuleGroup = [ - { - text: manualOrder[language], - rule: "FileManualOrder", - }, - ]; - - return [ - fileSortByNameRules, - fileSortByModifiedTimeRules, - fileSortByCreatedTimeRules, - filesManualSortRules, - ]; - }; + const getRuleGroups = () => + filesSortRulesGroup.map((rules) => + rules.map((rule) => ({ + rule, + text: FILE_SORT_RULES_COPY[rule][language], + })) + ); const onChangeSortRule = async (rule: FileSortRule) => { - if (rule === FILE_MANUAL_SORT_RULE - - ) { - await initFilesManualSortOrder(); - if (focusedFolder) { - const modal = new ManualSortFilesModal( - plugin, - focusedFolder, - useExplorerStore - ); - modal.open(); - } - } - changeFileSortRule(rule as FileSortRule); - }; + await changeFileSortRuleAndUpdateOrder(rule); - const getAriaLabel = () => { - return language === "zh" ? "对文件排序" : "Sort files"; + if (rule === FILE_MANUAL_SORT_RULE) { + const modal = new ManualSortFilesModal( + plugin, + useExplorerStore, + focusedFolder + ); + modal.open(); + } }; return ( ); }; diff --git a/src/components/FileTree/index.tsx b/src/components/FileTree/index.tsx index 23a4df4..5aa0b5e 100644 --- a/src/components/FileTree/index.tsx +++ b/src/components/FileTree/index.tsx @@ -16,7 +16,7 @@ type Props = { const FileTree = ({ onOpenFoldersPane = () => {} }: Props) => { const { useExplorerStore } = useExplorer(); - const { sortFiles, fileSortRule } = useExplorerStore( + const { sortFiles } = useExplorerStore( useShallow((store: ExplorerStore) => ({ sortFiles: store.sortFiles, fileSortRule: store.fileSortRule, @@ -44,7 +44,7 @@ const FileTree = ({ onOpenFoldersPane = () => {} }: Props) => { ); } - const sortedFiles = sortFiles(files, fileSortRule); + const sortedFiles = sortFiles(files); return (
diff --git a/src/components/Folder/Content.tsx b/src/components/Folder/Content.tsx index 995f98e..7bbd43c 100644 --- a/src/components/Folder/Content.tsx +++ b/src/components/Folder/Content.tsx @@ -10,13 +10,10 @@ import { useShowFolderIcon } from "src/hooks/useSettingsHandler"; import { FOLDER_OPERATION_COPY } from "src/locales"; import { ExplorerStore } from "src/store"; - import { FolderListModal } from "../FolderListModal"; - import FilesCount from "./FilesCount"; - export type FolderProps = { folder: TFolder; }; @@ -29,7 +26,7 @@ const FolderContent = ({ folder, onToggleExpandState }: Props) => { const { focusedFolder, - setFocusedFolder, + changeFocusedFolder, expandedFolderPaths, createNewFolder, createFile, @@ -44,7 +41,7 @@ const FolderContent = ({ folder, onToggleExpandState }: Props) => { } = useExplorerStore( useShallow((store: ExplorerStore) => ({ focusedFolder: store.focusedFolder, - setFocusedFolder: store.setFocusedFolder, + changeFocusedFolder: store.changeFocusedFolder, expandedFolderPaths: store.expandedFolderPaths, createNewFolder: store.createNewFolder, createFile: store.createFile, @@ -161,7 +158,7 @@ const FolderContent = ({ folder, onToggleExpandState }: Props) => { item.setTitle(FOLDER_OPERATION_COPY.createFile[language]); item.onClick(async () => { if (folder.path !== focusedFolder?.path) { - await setFocusedFolder(folder); + await changeFocusedFolder(folder); } await createFile(folder); }); @@ -175,7 +172,7 @@ const FolderContent = ({ folder, onToggleExpandState }: Props) => { onToggleExpandState(); } if (newFolder) { - await setFocusedFolder(newFolder); + await changeFocusedFolder(newFolder); } }); }); diff --git a/src/components/Folder/ExpandIcon.tsx b/src/components/Folder/ExpandIcon.tsx index 71c8bf9..d035870 100644 --- a/src/components/Folder/ExpandIcon.tsx +++ b/src/components/Folder/ExpandIcon.tsx @@ -12,9 +12,9 @@ type Props = { const FolderExpandIcon = ({ folder }: Props) => { const { useExplorerStore } = useExplorer(); - const { hasSubFolders, expandedFolderPaths } = useExplorerStore( + const { hasSubFolder, expandedFolderPaths } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - hasSubFolders: store.hasSubFolders, + hasSubFolder: store.hasSubFolder, expandedFolderPaths: store.expandedFolderPaths, })) ); @@ -24,7 +24,7 @@ const FolderExpandIcon = ({ folder }: Props) => { return ( ); }; diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index 4d83aac..82dbd52 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -28,19 +28,19 @@ const Folder = ({ const { language } = plugin; const { - setFocusedFolder, + changeFocusedFolder, expandFolder, collapseFolder, expandedFolderPaths, focusedFolder, - hasSubFolders, + hasSubFolder, getFilesinFolder, getSubFolders, changeViewMode, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - setFocusedFolder: store.setFocusedFolder, - hasSubFolders: store.hasSubFolders, + changeFocusedFolder: store.changeFocusedFolder, + hasSubFolder: store.hasSubFolder, expandedFolderPaths: store.expandedFolderPaths, expandFolder: store.expandFolder, collapseFolder: store.collapseFolder, @@ -132,7 +132,7 @@ const Folder = ({ return classNames( "ffs__folder-container tree-item-self nav-folder-title is-clickable", { - "mod-collapsible": hasSubFolders(folder), + "mod-collapsible": hasSubFolder(folder), "is-active": isFocused, "ffs__is-over": isOver && !disableDrag, } @@ -145,7 +145,7 @@ const Folder = ({ ref={setDropRef} onClick={async () => { onToggleExpandState(); - setFocusedFolder(folder); + changeFocusedFolder(folder); await changeViewMode("folder"); }} style={getIndentStyle()} diff --git a/src/components/FolderAndTagActions/CreateFolder.tsx b/src/components/FolderAndTagActions/CreateFolder.tsx index 5bbd953..26d186e 100644 --- a/src/components/FolderAndTagActions/CreateFolder.tsx +++ b/src/components/FolderAndTagActions/CreateFolder.tsx @@ -18,7 +18,7 @@ const CreateFolder = () => { focusedFolder, createNewFolder, changeExpandedFolderPaths, - setFocusedFolder, + changeFocusedFolder, expandedFolderPaths, initOrder, getNameOfFolder, @@ -28,7 +28,7 @@ const CreateFolder = () => { focusedFolder: store.focusedFolder, createNewFolder: store.createNewFolder, changeExpandedFolderPaths: store.changeExpandedFolderPaths, - setFocusedFolder: store.setFocusedFolder, + changeFocusedFolder: store.changeFocusedFolder, expandedFolderPaths: store.expandedFolderPaths, initOrder: store.initFoldersManualSortOrder, getNameOfFolder: store.getNameOfFolder, @@ -55,7 +55,7 @@ const CreateFolder = () => { const newFolder = await createNewFolder(parentFolder); if (newFolder) { await expandParentFolders(parentFolder); - await setFocusedFolder(newFolder); + await changeFocusedFolder(newFolder); } await initOrder(); }; diff --git a/src/components/FolderAndTagActions/SortFoldersAndTags.tsx b/src/components/FolderAndTagActions/SortFoldersAndTags.tsx index e3395db..13fc22c 100644 --- a/src/components/FolderAndTagActions/SortFoldersAndTags.tsx +++ b/src/components/FolderAndTagActions/SortFoldersAndTags.tsx @@ -1,110 +1,59 @@ import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import { FOLDER_SORT_RULES_COPY } from "src/locales"; +import { FOLDER_SORT_RULES_COPY, SORT_TIPS_COPY } from "src/locales"; import { ExplorerStore } from "src/store"; import { FOLDER_MANUAL_SORT_RULE, FolderSortRule } from "src/store/folder/sort"; import { ManualSortFoldersModal } from "../ManualSortFoldersModal"; import SortAction from "../SortAction"; -type FolderSortRuleItem = { - text: string; - rule: FolderSortRule; -}; -type FolderSortRuleGroup = FolderSortRuleItem[]; - const SortFolders = () => { const { useExplorerStore, plugin } = useExplorer(); const { language } = plugin; const { folderSortRule, - changeFolderSortRule, - isFoldersInAscendingOrder, - initFoldersManualSortOrder, + folderSortRulesGroup, + changeFolderSortRuleAndUpdateOrder, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ folderSortRule: store.folderSortRule, - isFoldersInAscendingOrder: store.isFoldersInAscendingOrder, - changeFolderSortRule: store.changeFolderSortRule, - initFoldersManualSortOrder: store.initFoldersManualSortOrder, + folderSortRulesGroup: store.folderSortRulesGroup, + changeFolderSortRuleAndUpdateOrder: + store.changeFolderSortRuleAndUpdateOrder, })) ); - const getRuleGroups = () => { - const { - folderNameAscending, - folderNameDescending, - filesCountAscending, - filesCountDescending, - manualOrder, - } = FOLDER_SORT_RULES_COPY; - const folderSortByNameRules: FolderSortRuleGroup = [ - { - text: folderNameAscending[language], - rule: "FolderNameAscending", - }, - { - text: folderNameDescending[language], - rule: "FolderNameDescending", - }, - ]; - const folderSortByFilesCountRules: FolderSortRuleGroup = [ - { - text: filesCountAscending[language], - rule: "FilesCountAscending", - }, - { - text: filesCountDescending[language], - rule: "FilesCountDescending", - }, - ]; - const folderManualSortRules: FolderSortRuleGroup = [ - { - text: manualOrder[language], - rule: "FolderManualOrder", - }, - ]; - return [ - folderSortByNameRules, - folderSortByFilesCountRules, - folderManualSortRules, - ]; - }; + const getRuleGroups = () => + folderSortRulesGroup.map((rules) => + rules.map((rule) => ({ + rule, + text: FOLDER_SORT_RULES_COPY[rule][language], + })) + ); + + const onChangeSortRule = async (rule: FolderSortRule) => { + await changeFolderSortRuleAndUpdateOrder(rule); - const onChangeSortRule = (rule: FolderSortRule) => { if (rule === FOLDER_MANUAL_SORT_RULE) { - initFoldersManualSortOrder(); const modal = new ManualSortFoldersModal( plugin, - plugin.app.vault.getRoot(), - useExplorerStore + useExplorerStore, + plugin.app.vault.getRoot() ); modal.open(); } - if (rule !== folderSortRule) { - changeFolderSortRule(rule as FolderSortRule); - } - }; - - const getAriaLabel = () => { - return language === "zh" - ? "对文件夹和标签排序" - : "Sort folders and tags"; }; return ( ); }; diff --git a/src/components/FolderAndTagActions/ToggleFoldersAndTags.tsx b/src/components/FolderAndTagActions/ToggleFoldersAndTags.tsx index 6ff12e4..9b4fadb 100644 --- a/src/components/FolderAndTagActions/ToggleFoldersAndTags.tsx +++ b/src/components/FolderAndTagActions/ToggleFoldersAndTags.tsx @@ -36,10 +36,10 @@ const ToggleFolders = () => { const [isExpanded, setIsExpanded] = useState(false); useEffect(() => { - const isFolderExpanded = + const hassFolderExpanded = showFolderView && expandedFolderPaths.length > 0; - const isTagExpanded = showTagView && expandedTagPaths.length > 0; - setIsExpanded(isFolderExpanded || isTagExpanded); + const hasTagExpanded = showTagView && expandedTagPaths.length > 0; + setIsExpanded(hassFolderExpanded || hasTagExpanded); }, [expandedFolderPaths, expandedTagPaths, showFolderView, showTagView]); const onToggleAllFolders = () => { diff --git a/src/components/FolderAndTagTree/index.tsx b/src/components/FolderAndTagTree/index.tsx index 01c9f63..76d8de2 100644 --- a/src/components/FolderAndTagTree/index.tsx +++ b/src/components/FolderAndTagTree/index.tsx @@ -28,7 +28,7 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => { const { rootFolder, folderSortRule, - hasSubFolders, + hasSubFolder, getSubFolders, sortFolders, expandedFolderPaths, @@ -37,14 +37,14 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => { sortTags, expandedTagPaths, getTagsByParent, - hasTagChildren, + hasSubTag, focusedTag, isTopLevelTag, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ rootFolder: store.rootFolder, folderSortRule: store.folderSortRule, - hasSubFolders: store.hasSubFolders, + hasSubFolder: store.hasSubFolder, getSubFolders: store.getSubFolders, sortFolders: store.sortFolders, expandedFolderPaths: store.expandedFolderPaths, @@ -55,7 +55,7 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => { sortTags: store.sortTags, expandedTagPaths: store.expandedTagPaths, getTagsByParent: store.getTagsByParent, - hasTagChildren: store.hasTagChildren, + hasSubTag: store.hasSubTag, focusedTag: store.focusedTag, isTopLevelTag: store.isTopLevelTag, })) @@ -127,7 +127,7 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => { > {renderFolder(folder)} {isExpanded && - hasSubFolders(folder) && + hasSubFolder(folder) && renderSubfolders(renderFolders(getSubFolders(folder)))}
); @@ -195,7 +195,7 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => { > {renderTag(tag)} {isExpanded && - hasTagChildren(tag) && + hasSubTag(tag) && renderSubTags( renderTags(getTagsByParent(tag.fullPath)) )} diff --git a/src/components/ManualSortFilesModal/ManualSortFiles.tsx b/src/components/ManualSortFilesModal/ManualSortFiles.tsx index 60c8c32..3dd56f8 100644 --- a/src/components/ManualSortFilesModal/ManualSortFiles.tsx +++ b/src/components/ManualSortFilesModal/ManualSortFiles.tsx @@ -30,16 +30,14 @@ type Props = { plugin: FolderFileSplitterPlugin; }; const ManualSortFiles = ({ parentFolder, useExplorerStore, plugin }: Props) => { - const { getFilesInFolder, sortFiles, fileSortRule, changeOrder } = - useExplorerStore( - useShallow((store: ExplorerStore) => ({ - order: store.filesManualSortOrder, - getFilesInFolder: store.getFilesInFolder, - sortFiles: store.sortFiles, - fileSortRule: store.fileSortRule, - changeOrder: store.changeFilesManualOrderAndSave, - })) - ); + const { getFilesInFolder, sortFiles, changeOrder } = useExplorerStore( + useShallow((store: ExplorerStore) => ({ + order: store.filesManualSortOrder, + getFilesInFolder: store.getFilesInFolder, + sortFiles: store.sortFiles, + changeOrder: store.moveFileInManualOrder, + })) + ); const [activeFile, setActiveFile] = useState(null); @@ -53,7 +51,7 @@ const ManualSortFiles = ({ parentFolder, useExplorerStore, plugin }: Props) => { const getSortedFiles = () => { if (!parentFolder) return []; const files = getFilesInFolder(parentFolder); - return sortFiles(files, fileSortRule); + return sortFiles(files); }; const onDragStart = (event: DragStartEvent) => { diff --git a/src/components/ManualSortFilesModal/index.tsx b/src/components/ManualSortFilesModal/index.tsx index 593ae9a..91a250c 100644 --- a/src/components/ManualSortFilesModal/index.tsx +++ b/src/components/ManualSortFilesModal/index.tsx @@ -6,7 +6,6 @@ import { TIPS_COPY } from "src/locales"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from "src/store"; - import ManualSortFiles from "./ManualSortFiles"; export class ManualSortFilesModal extends Modal { @@ -17,8 +16,8 @@ export class ManualSortFilesModal extends Modal { constructor( plugin: FolderFileSplitterPlugin, - folder: TFolder, - useExplorerStore: UseBoundStore> + useExplorerStore: UseBoundStore>, + folder: TFolder | null = plugin.app.vault.getRoot() ) { super(plugin.app); this.plugin = plugin; diff --git a/src/components/ManualSortFoldersModal/ManualSortFolders.tsx b/src/components/ManualSortFoldersModal/ManualSortFolders.tsx index 36f3463..461f9f0 100644 --- a/src/components/ManualSortFoldersModal/ManualSortFolders.tsx +++ b/src/components/ManualSortFoldersModal/ManualSortFolders.tsx @@ -38,15 +38,16 @@ const ManualSortFolders = ({ getSubFolders, sortFolders, folderSortRule, - changeFoldersManualOrderAndSave, + moveFolderInManualOrder, + findFolderByPath, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ getSubFolders: store.getSubFolders, sortFolders: store.sortFolders, folderSortRule: store.folderSortRule, order: store.foldersManualSortOrder, - changeFoldersManualOrderAndSave: - store.changeFoldersManualOrderAndSave, + moveFolderInManualOrder: store.moveFolderInManualOrder, + findFolderByPath: store.findFolderByPath, })) ); @@ -67,7 +68,7 @@ const ManualSortFolders = ({ const getSortedFolders = () => { if (!folder) return []; - return sortFolders(getSubFolders(folder), folderSortRule, false); + return sortFolders(getSubFolders(folder)); }; const onDragStart = (event: DragStartEvent) => { @@ -83,7 +84,7 @@ const ManualSortFolders = ({ const atIndex = getSortedFolders().findIndex( (f) => f.path === over.id ); - return await changeFoldersManualOrderAndSave(folder, atIndex); + return await moveFolderInManualOrder(folder, atIndex); } }; @@ -122,7 +123,7 @@ const ManualSortFolders = ({ {renderSlashSign()} {crumbs.map((crumb, index) => { const path = crumbs.slice(0, index + 1).join("/"); - const target = plugin.app.vault.getFolderByPath(path); + const target = findFolderByPath(path); return ( {index > 0 && renderSlashSign()} diff --git a/src/components/ManualSortFoldersModal/index.tsx b/src/components/ManualSortFoldersModal/index.tsx index de6e644..e700d5a 100644 --- a/src/components/ManualSortFoldersModal/index.tsx +++ b/src/components/ManualSortFoldersModal/index.tsx @@ -17,8 +17,8 @@ export class ManualSortFoldersModal extends Modal { constructor( plugin: FolderFileSplitterPlugin, + useExplorerStore: UseBoundStore>, folder: TFolder, - useExplorerStore: UseBoundStore> ) { super(plugin.app); this.plugin = plugin; diff --git a/src/components/SortAction.tsx b/src/components/SortAction.tsx index 4b4f4c1..80cd0c1 100644 --- a/src/components/SortAction.tsx +++ b/src/components/SortAction.tsx @@ -5,59 +5,57 @@ import { AscendingSortIcon, DescendingSortIcon, } from "src/assets/icons"; -import FolderFileSplitterPlugin from "src/main"; +import { useExplorer } from "src/hooks/useExplorer"; +import { + addMenuItem, + isInAscendingOrderRule, + isManualSortOrderRule, + SortRule, + triggerMenu, +} from "src/utils"; -type SortRule = { +type RuleCopy = { text: string; rule: string; }; -type SortRuleGroup = SortRule[]; +type SortRuleGroup = RuleCopy[]; type Props = { - plugin: FolderFileSplitterPlugin; ruleGroups: SortRuleGroup[]; menuName: string; changeSortRule: (rule: string) => void; - isInAscendingOrder: () => boolean; - currentSortRule: string; - isManualOrder: boolean; + currentSortRule: SortRule; }; const SortAction = ({ - plugin, ruleGroups, menuName, changeSortRule, - isInAscendingOrder, currentSortRule, - isManualOrder, ...props }: Props) => { + const { plugin } = useExplorer(); + const onChangeSortRule = (e: React.MouseEvent) => { const menu = new Menu(); ruleGroups.forEach((rules) => { rules.forEach(({ text, rule }) => { - menu.addItem((newItem) => { - newItem - .setTitle(text) - .setChecked(rule === currentSortRule) - .onClick(() => { - changeSortRule(rule); - }); + addMenuItem(menu, { + title: text, + checked: rule === currentSortRule, + action: () => changeSortRule(rule), }); }); menu.addSeparator(); }); - plugin.app.workspace.trigger(menuName, menu); - menu.showAtPosition({ x: e.pageX, y: e.pageY }); - return false; + triggerMenu(plugin, menu, menuName)(e); }; const renderIcon = () => { const actionButtonClassName = "ffs__action-button svg-icon"; - if (isManualOrder) { + if (isManualSortOrderRule(currentSortRule)) { return ; } - return isInAscendingOrder() ? ( + return isInAscendingOrderRule(currentSortRule) ? ( ) : ( diff --git a/src/components/Tag/Content.tsx b/src/components/Tag/Content.tsx index 4b36dd1..e50fd34 100644 --- a/src/components/Tag/Content.tsx +++ b/src/components/Tag/Content.tsx @@ -84,7 +84,7 @@ const TagContent = ({ tag }: Props) => { const menu = new Menu(); menu.addItem((item) => { - const isPinned = isTagPinned(tag.fullPath); + const isPinned = isTagPinned(tag); item.setIcon(isPinned ? "pin-off" : "pin"); const title = isPinned ? TAG_OPERATION_COPY.unpinTag[language] @@ -92,9 +92,9 @@ const TagContent = ({ tag }: Props) => { item.setTitle(title); item.onClick(() => { if (isPinned) { - unpinTag(tag.fullPath); + unpinTag(tag); } else { - pinTag(tag.fullPath); + pinTag(tag); } }); }); diff --git a/src/components/Tag/ExpandIcon.tsx b/src/components/Tag/ExpandIcon.tsx index fcbc650..d7b85ef 100644 --- a/src/components/Tag/ExpandIcon.tsx +++ b/src/components/Tag/ExpandIcon.tsx @@ -4,29 +4,25 @@ import { useExplorer } from "src/hooks/useExplorer"; import { ExplorerStore } from "src/store"; import { TagNode } from "src/store/tag"; - import ExpandIcon from "../ExpandIcon"; - type Props = { tag: TagNode; }; const TagExpandIcon = ({ tag }: Props) => { const { useExplorerStore } = useExplorer(); - const { hasTagChildren, expandedTagPaths } = useExplorerStore( + const { hasSubTag, isTagExpanded } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - hasTagChildren: store.hasTagChildren, - expandedTagPaths: store.expandedTagPaths, + hasSubTag: store.hasSubTag, + isTagExpanded: store.isTagExpanded, })) ); - const isTagExpanded = expandedTagPaths.includes(tag.fullPath); - return ( ); }; diff --git a/src/components/Tag/FilesCount.tsx b/src/components/Tag/FilesCount.tsx index ccb63f6..aea8054 100644 --- a/src/components/Tag/FilesCount.tsx +++ b/src/components/Tag/FilesCount.tsx @@ -1,4 +1,3 @@ - import { useEffect, useState } from "react"; import { useShallow } from "zustand/react/shallow"; diff --git a/src/components/Tag/index.tsx b/src/components/Tag/index.tsx index 37ba8ac..e6ce52d 100644 --- a/src/components/Tag/index.tsx +++ b/src/components/Tag/index.tsx @@ -23,7 +23,7 @@ const Tag = ({ const { language } = plugin; const { - hasTagChildren, + hasSubTag, setFocusedTag, focusedTag, getFilesInTag, @@ -34,7 +34,7 @@ const Tag = ({ changeViewMode, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - hasTagChildren: store.hasTagChildren, + hasSubTag: store.hasSubTag, setFocusedTag: store.setFocusedTag, focusedTag: store.focusedTag, getTagsByParent: store.getTagsByParent, @@ -94,7 +94,7 @@ const Tag = ({ return classNames( "ffs__folder-container tree-item-self nav-folder-title is-clickable", { - "mod-collapsible": hasTagChildren(tag), + "mod-collapsible": hasSubTag(tag), "is-active": isFocused, } ); diff --git a/src/hooks/useChangeActiveLeaf.tsx b/src/hooks/useChangeActiveLeaf.tsx index a991cbd..1386bc2 100644 --- a/src/hooks/useChangeActiveLeaf.tsx +++ b/src/hooks/useChangeActiveLeaf.tsx @@ -4,22 +4,24 @@ import { useShallow } from "zustand/react/shallow"; import { ActiveLeafChangeEventName } from "src/assets/constants"; import { ExplorerStore } from "src/store"; - - import { useExplorer } from "./useExplorer"; const useChangeActiveLeaf = () => { const { useExplorerStore, plugin } = useExplorer(); - const { focusedFile, setFocusedFile, expandFolder, setFocusedFolder } = - useExplorerStore( - useShallow((store: ExplorerStore) => ({ - focusedFile: store.focusedFile, - expandFolder: store.expandFolder, - setFocusedFolder: store.setFocusedFolder, - setFocusedFile: store.setFocusedFile, - })) - ); + const { + focusedFile, + setFocusedFileAndSave, + expandFolder, + changeFocusedFolder, + } = useExplorerStore( + useShallow((store: ExplorerStore) => ({ + focusedFile: store.focusedFile, + expandFolder: store.expandFolder, + changeFocusedFolder: store.changeFocusedFolder, + setFocusedFileAndSave: store.setFocusedFileAndSave, + })) + ); useEffect(() => { window.addEventListener( @@ -35,7 +37,7 @@ const useChangeActiveLeaf = () => { }, [focusedFile]); const onHandleActiveLeafChange = async () => { - if (!plugin.settings.revealFileInExplorer) return + if (!plugin.settings.revealFileInExplorer) return; const currentActiveFile = plugin.app.workspace.getActiveFile(); if (currentActiveFile && currentActiveFile.path !== focusedFile?.path) { let currentFolder = currentActiveFile.parent; @@ -43,8 +45,8 @@ const useChangeActiveLeaf = () => { expandFolder(currentFolder); currentFolder = currentFolder.parent; } - setFocusedFolder(currentActiveFile.parent); - await setFocusedFile(currentActiveFile); + changeFocusedFolder(currentActiveFile.parent); + await setFocusedFileAndSave(currentActiveFile); } }; }; diff --git a/src/hooks/useVaultChangeHandler/useChangeFile.tsx b/src/hooks/useVaultChangeHandler/useChangeFile.tsx index 3ba4f3c..0a3450c 100644 --- a/src/hooks/useVaultChangeHandler/useChangeFile.tsx +++ b/src/hooks/useVaultChangeHandler/useChangeFile.tsx @@ -20,7 +20,7 @@ const useChangeFile = () => { focusedFolder, focusedTag, updatePinnedFilePath, - updateFileManualOrder, + updateFilePathInManualOrder, fileSortRule, initOrder, isFilePinned, @@ -31,7 +31,7 @@ const useChangeFile = () => { focusedFolder: store.focusedFolder, focusedTag: store.focusedTag, updatePinnedFilePath: store.updatePinnedFilePath, - updateFileManualOrder: store.updateFileManualOrder, + updateFilePathInManualOrder: store.updateFilePathInManualOrder, fileSortRule: store.fileSortRule, initOrder: store.initFilesManualSortOrder, isFilePinned: store.isFilePinned, @@ -92,7 +92,7 @@ const useChangeFile = () => { const parentPath = file.parent?.path; await updatePinnedFilePath(oldPath, file.path); if (parentPath && fileSortRule === FILE_MANUAL_SORT_RULE) { - await updateFileManualOrder( + await updateFilePathInManualOrder( parentPath, oldPath, file.path diff --git a/src/hooks/useVaultChangeHandler/useChangeFolder.tsx b/src/hooks/useVaultChangeHandler/useChangeFolder.tsx index 274abf5..e058686 100644 --- a/src/hooks/useVaultChangeHandler/useChangeFolder.tsx +++ b/src/hooks/useVaultChangeHandler/useChangeFolder.tsx @@ -4,10 +4,10 @@ import { useShallow } from "zustand/react/shallow"; import { VaultChangeEvent, VaultChangeEventName } from "src/assets/constants"; import { ExplorerStore } from "src/store"; -import { isFolder, removeItemFromArray } from "src/utils"; +import { FOLDER_MANUAL_SORT_RULE } from "src/store/folder/sort"; +import { isFolder } from "src/utils"; import { useExplorer } from "../useExplorer"; -import { FOLDER_MANUAL_SORT_RULE } from "src/store/folder/sort"; const useChangeFolder = () => { const { useExplorerStore } = useExplorer(); @@ -19,7 +19,7 @@ const useChangeFolder = () => { isFolderPinned, unpinFolder, updatePinnedFolderPath, - updateFolderManualOrder, + updateFolderPathInManualOrder, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ getTopLevelFolders: store.getTopLevelFolders, @@ -28,7 +28,7 @@ const useChangeFolder = () => { isFolderPinned: store.isFolderPinned, unpinFolder: store.unpinFolder, updatePinnedFolderPath: store.updatePinnedFolderPath, - updateFolderManualOrder: store._updateFolderManualOrder, + updateFolderPathInManualOrder: store.updateFolderPathInManualOrder, })) ); @@ -89,7 +89,11 @@ const useChangeFolder = () => { await updatePinnedFolderPath(oldPath, folder.path); if (!parentPath || folderSortRule !== FOLDER_MANUAL_SORT_RULE) return; - await updateFolderManualOrder(parentPath, oldPath, folder.path); + await updateFolderPathInManualOrder( + parentPath, + oldPath, + folder.path + ); break; case "modify": onUpdateTopFolders(); diff --git a/src/locales/index.ts b/src/locales/index.ts index 0bad2d6..a16e902 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -126,54 +126,54 @@ export const TIPS_COPY: Copy = { }; export const FOLDER_SORT_RULES_COPY: Copy = { - folderNameAscending: { + FolderNameAscending: { en: "Folder/Tag name(A to Z)", zh: "按文件夹/标签名升序", }, - folderNameDescending: { + FolderNameDescending: { en: "Folder/Tag name(Z to A)", zh: "按文件夹/标签名降序", }, - filesCountAscending: { + FilesCountAscending: { en: "Files count(small to large)", zh: "按文件数(从少到多)", }, - filesCountDescending: { + FilesCountDescending: { en: "Files count(large to small)", zh: "按文件数(从多到少)", }, - manualOrder: { + FolderManualOrder: { en: "Manual order", zh: "手动排序", }, }; export const FILE_SORT_RULES_COPY: Copy = { - fileNameAscending: { + FileNameAscending: { en: "File name(A to Z)", zh: "按文件名升序", }, - fileNameDescending: { + FileNameDescending: { en: "File name(Z to A)", zh: "按文件名降序", }, - modifiledTimeDescending: { + FileModifiedTimeDescending: { en: "Modifiled time(new to old)", zh: "按修改时间(从新到旧)", }, - modifiledTimeAscending: { + FileModifiedTimeAscending: { en: "Modifiled time(old to new)", zh: "按修改时间(从旧到新)", }, - createdTimeDescending: { + FileCreatedTimeDescending: { en: "Created time(new to old)", zh: "按创建时间(从新到旧)", }, - createdTimeAscending: { + FileCreatedTimeAscending: { en: "Created time(old to new)", zh: "按创建时间(从旧到新)", }, - manualOrder: { + FileManualOrder: { en: "Manual order", zh: "手动排序", }, @@ -197,3 +197,14 @@ export const VERTICAL_SPLIT_LAYOUT_OPERATION_COPY: Copy = { zh: "关闭文件列表", }, }; + +export const SORT_TIPS_COPY: Copy = { + sortFiles: { + en: "Sort files", + zh: "对文件排序", + }, + sortFoldersAndTags: { + en: "Sort folders and tags", + zh: "对文件夹和标签排序", + }, +}; diff --git a/src/store/common.ts b/src/store/common.ts deleted file mode 100644 index 898afd0..0000000 --- a/src/store/common.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { StateCreator } from "zustand"; - -import { FFS_VIEW_MODE_KEY } from "src/assets/constants"; -import { ValueOf } from "src/settings"; -import { ExplorerStore } from "src/store"; -import { logError, uniq } from "src/utils"; - -import FolderFileSplitterPlugin from "../main"; - -export const VIEW_MODE = { - ALL: "all", - TAG: "tag", - FOLDER: "folder", -}; -export type ViewMode = ValueOf; -export const DEFAULT_VIEW_MODE: ViewMode = VIEW_MODE.FOLDER; - -type FolderPath = string; -type ChildrenPaths = string[]; -export type ManualSortOrder = Record; - -type SetValueAndSaveInLocalStorageParams = { - key: string; - value: T; - localStorageKey: string; - localStorageValue: string; -}; -type SetValueAndSaveInPluginParams = { - key: string; - value: T; - pluginKey: string; - pluginValue: T | string; -}; -type RestoreDataFromLocalStorageParams = { - localStorageKey: string; - key: string; - needParse?: boolean; - transform?: (value: any) => unknown; -}; -type RestoreDataFromPluginParams = { - pluginKey: string; - key: string; - needParse?: boolean; -}; - -export type CommonExplorerStore = { - viewMode: ViewMode; - changeViewMode: (mode: ViewMode) => void; - restoreViewMode: () => void; - - getDataFromLocalStorage: (key: string) => string | null; - saveDataInLocalStorage: (key: string, value: string) => void; - removeDataFromLocalStorage: (key: string) => void; - - getDataFromPlugin: (key: string) => Promise; - saveDataInPlugin: (oata: Record) => Promise; - - setValueAndSaveInLocalStorage: ({ - key, - value, - localStorageKey, - localStorageValue, - }: SetValueAndSaveInLocalStorageParams) => void; - setValueAndSaveInPlugin: ({ - key, - value, - pluginKey, - pluginValue, - }: SetValueAndSaveInPluginParams) => Promise; - restoreDataFromLocalStorage: ({ - localStorageKey, - key, - needParse, - transform, - }: RestoreDataFromLocalStorageParams) => T; - restoreDataFromPlugin: ({ - pluginKey, - key, - needParse, - }: RestoreDataFromPluginParams) => Promise; -}; - -export const createCommonExplorerStore = - ( - plugin: FolderFileSplitterPlugin - ): StateCreator => - (set, get) => ({ - viewMode: DEFAULT_VIEW_MODE, - - changeViewMode: (mode: ViewMode) => { - const { setValueAndSaveInLocalStorage } = get(); - setValueAndSaveInLocalStorage({ - key: "viewMode", - value: mode, - localStorageKey: FFS_VIEW_MODE_KEY, - localStorageValue: mode, - }); - }, - restoreViewMode: () => { - const { getDataFromLocalStorage } = get(); - const viewMode = getDataFromLocalStorage(FFS_VIEW_MODE_KEY); - if (!viewMode) return; - set({ - viewMode, - }); - }, - - saveDataInLocalStorage: (key: string, value: string) => { - localStorage.setItem(key, value); - }, - getDataFromLocalStorage: (key: string) => { - return localStorage.getItem(key); - }, - removeDataFromLocalStorage: (key: string) => { - localStorage.removeItem(key); - }, - - saveDataInPlugin: async ( - data: Record - ): Promise => { - const previousData = await plugin.loadData(); - await plugin.saveData({ - ...previousData, - ...data, - }); - }, - getDataFromPlugin: async (key: string): Promise => { - try { - const data = await plugin.loadData(); - return data[key]; - } catch (e) { - console.error(e); - return undefined; - } - }, - - setValueAndSaveInLocalStorage: ({ - key, - value, - localStorageKey, - localStorageValue, - }: SetValueAndSaveInLocalStorageParams) => { - const { saveDataInLocalStorage, removeDataFromLocalStorage } = - get(); - - set({ [key]: value } as Partial); - - if (value === null || value === undefined) { - removeDataFromLocalStorage(localStorageKey); - } else { - saveDataInLocalStorage(localStorageKey, localStorageValue); - } - }, - setValueAndSaveInPlugin: async ({ - key, - value, - pluginKey, - pluginValue, - }: SetValueAndSaveInPluginParams): Promise => { - const { saveDataInPlugin } = get(); - set({ [key]: value } as Partial); - await saveDataInPlugin({ [pluginKey]: pluginValue }); - }, - - restoreDataFromLocalStorage: ({ - localStorageKey, - key, - needParse = false, - transform, - }: RestoreDataFromLocalStorageParams) => { - const { getDataFromLocalStorage } = get(); - const raw = getDataFromLocalStorage(localStorageKey); - if (!raw) return; - - try { - const parsed = needParse ? JSON.parse(raw) : raw; - const removeDuplicate = Array.isArray(parsed) - ? uniq(parsed) - : parsed; - const finalData = transform - ? transform(removeDuplicate) - : parsed; - - set({ [key]: finalData } as Partial); - return finalData; - } catch (error) { - logError({ - name: "restoreDataFromLocalStorage", - error, - data: raw, - params: { - key, - localStorageKey, - needParse, - hasTransform: !!transform, - }, - }); - } - }, - restoreDataFromPlugin: async ({ - pluginKey, - key, - needParse = false, - }: RestoreDataFromPluginParams) => { - const { getDataFromPlugin } = get(); - const raw = await getDataFromPlugin(pluginKey); - if (!raw) return; - - try { - const parsed = needParse ? JSON.parse(raw) : raw; - const final = Array.isArray(parsed) ? uniq(parsed) : parsed; - set({ [key]: final } as Partial); - return parsed; - } catch (error) { - logError({ - name: "restoreDataFromPlugin", - error, - data: raw, - params: { - key, - pluginKey, - needParse, - }, - }); - } - }, - }); diff --git a/src/store/common/index.ts b/src/store/common/index.ts new file mode 100644 index 0000000..c6f7e5b --- /dev/null +++ b/src/store/common/index.ts @@ -0,0 +1,39 @@ +import { StateCreator } from "zustand"; + +import { ExplorerStore } from "src/store"; + +import FolderFileSplitterPlugin from "../../main"; + +import { createLocalStorageSlice, LocalStorageSlice } from "./localStorage"; +import { createManualSortSlice, ManualSortSlice } from "./manualSort"; +import { createPinSlice, PinSlice } from "./pin"; +import { createPluginSlice, PluginSlice } from "./plugin"; +import { createViewModeSlice, ViewModeSlice } from "./viewMode"; + +type FolderPath = string; +type ChildrenPaths = string[]; +export type ManualSortOrder = Record; +export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {}; + +export type CommonExplorerStore = ViewModeSlice & + LocalStorageSlice & + PluginSlice & + ManualSortSlice & + PinSlice; + +export const createCommonExplorerStore = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get, store) => ({ + ...createViewModeSlice(plugin)(set, get, store), + ...createLocalStorageSlice(plugin)(set, get, store), + ...createPluginSlice(plugin)(set, get, store), + ...createManualSortSlice(plugin)(set, get, store), + ...createPinSlice(plugin)(set, get, store), + }); + +export * from "./viewMode"; +export * from "./localStorage"; +export * from "./plugin"; +export * from "./manualSort"; diff --git a/src/store/common/localStorage.ts b/src/store/common/localStorage.ts new file mode 100644 index 0000000..760652d --- /dev/null +++ b/src/store/common/localStorage.ts @@ -0,0 +1,116 @@ +import { StateCreator } from "zustand"; + +import { ExplorerStore } from "src/store"; +import { logError, uniq } from "src/utils"; + +import FolderFileSplitterPlugin from "../../main"; + +type FolderPath = string; +type ChildrenPaths = string[]; +export type ManualSortOrder = Record; +export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {}; + +type SetValueAndSaveInLocalStorageParams = { + key: string; + value: T; + localStorageKey: string; + localStorageValue: string; +}; + +type RestoreDataFromLocalStorageParams = { + localStorageKey: string; + key: string; + needParse?: boolean; + transform?: (value: unknown) => unknown; + validate?: (value: unknown) => boolean; +}; +export type LocalStorageSlice = { + getDataFromLocalStorage: (key: string) => string | null; + saveDataInLocalStorage: (key: string, value: string) => void; + removeDataFromLocalStorage: (key: string) => void; + + setValueAndSaveInLocalStorage: ({ + key, + value, + localStorageKey, + localStorageValue, + }: SetValueAndSaveInLocalStorageParams) => void; + restoreDataFromLocalStorage: ({ + localStorageKey, + key, + needParse, + transform, + }: RestoreDataFromLocalStorageParams) => T; +}; + +export const createLocalStorageSlice = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get) => ({ + saveDataInLocalStorage: (key: string, value: string) => { + localStorage.setItem(key, value); + }, + getDataFromLocalStorage: (key: string) => { + return localStorage.getItem(key); + }, + removeDataFromLocalStorage: (key: string) => { + localStorage.removeItem(key); + }, + + setValueAndSaveInLocalStorage: ({ + key, + value, + localStorageKey, + localStorageValue, + }: SetValueAndSaveInLocalStorageParams) => { + const { saveDataInLocalStorage, removeDataFromLocalStorage } = + get(); + + set({ [key]: value } as Partial); + + if (value === null || value === undefined) { + removeDataFromLocalStorage(localStorageKey); + } else { + saveDataInLocalStorage(localStorageKey, localStorageValue); + } + }, + restoreDataFromLocalStorage: ({ + localStorageKey, + key, + needParse = false, + transform, + validate, + }: RestoreDataFromLocalStorageParams) => { + const { getDataFromLocalStorage } = get(); + const raw = getDataFromLocalStorage(localStorageKey); + if (!raw) return; + + try { + const parsed = needParse ? JSON.parse(raw) : raw; + const removeDuplicate = Array.isArray(parsed) + ? uniq(parsed) + : parsed; + const finalData = transform + ? transform(removeDuplicate) + : removeDuplicate; + + if (validate && !validate(finalData)) return; + + set({ [key]: finalData } as Partial); + return finalData; + } catch (error) { + logError({ + name: "restoreDataFromLocalStorage", + error, + data: raw, + params: { + key, + localStorageKey, + needParse, + hasTransform: !!transform, + }, + }); + } + }, + }); diff --git a/src/store/common/manualSort.ts b/src/store/common/manualSort.ts new file mode 100644 index 0000000..f6b5425 --- /dev/null +++ b/src/store/common/manualSort.ts @@ -0,0 +1,177 @@ +import { TAbstractFile, TFolder } from "obsidian"; +import { StateCreator } from "zustand"; + +import { ExplorerStore } from "src/store"; +import { appendMissingItems, areArraysEqual, moveItemInArray } from "src/utils"; + +import FolderFileSplitterPlugin from "../../main"; + +type FolderPath = string; +type ChildrenPaths = string[]; +export type ManualSortOrder = Record; +export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {}; + +type UpdateOrderFn = (updatedOrder: ManualSortOrder) => Promise; +type UpdatePathInManualOrderPathParams = { + parentPath: string; + oldPath: string; + newPath: string; +}; + +export type ManualSortSlice = { + getInitialOrder: ( + folders: TFolder[], + getItems: (folder: TFolder) => TAbstractFile[], + sortItems: (items: TAbstractFile[]) => TAbstractFile[] + ) => ManualSortOrder; + getRestoredOrder: (pluginKey: string) => Promise; + resolveValidManualSortOrder: ( + restoredOrder: ManualSortOrder, + initialOrder: ManualSortOrder, + isPathValid: (path: string) => boolean + ) => ManualSortOrder; + getUpdatedPaths: ( + paths: string[], + pathToUpdate: string, + toIndex: number + ) => string[]; + moveItemInManualOrder: ( + order: ManualSortOrder, + item: TAbstractFile, + atIndex: number, + updatedOrder: UpdateOrderFn + ) => Promise; + updatePathInManualOrder: ( + order: ManualSortOrder, + updateOrder: UpdateOrderFn, + params: UpdatePathInManualOrderPathParams + ) => Promise; +}; + +export const createManualSortSlice = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get) => ({ + getInitialOrder: ( + folders: TFolder[], + getItems: (folder: TFolder) => TAbstractFile[], + sortItems: (items: TAbstractFile[]) => TAbstractFile[] + ) => { + const order: ManualSortOrder = DEFAULT_MANUAL_SORT_ORDER; + + folders.forEach((folder) => { + const items = getItems(folder); + if (!items.length) return; + + const sortedItems = sortItems(items); + order[folder.path] = sortedItems.map((i) => i.path); + }); + return order; + }, + getRestoredOrder: async (pluginKey: string) => { + const { getDataFromPlugin } = get(); + return ( + (await getDataFromPlugin(pluginKey)) ?? + DEFAULT_MANUAL_SORT_ORDER + ); + }, + + resolveValidManualSortOrder: ( + restoredOrder: ManualSortOrder, + initialOrder: ManualSortOrder, + isPathValid: (path: string) => boolean + ): ManualSortOrder => { + const finalOrder = DEFAULT_MANUAL_SORT_ORDER; + + const folderPaths = Object.keys(initialOrder); + if (!folderPaths.length) { + return finalOrder; + } + if (!Object.keys(restoredOrder).length) { + return initialOrder; + } + for (const folderPath of folderPaths) { + const restoredPaths = restoredOrder[folderPath] ?? []; + const currentPaths = initialOrder[folderPath] ?? []; + + if (!restoredPaths.length || !currentPaths.length) { + finalOrder[folderPath] = currentPaths; + continue; + } + + const validRestoredPaths = restoredPaths.filter(isPathValid); + + finalOrder[folderPath] = appendMissingItems( + validRestoredPaths, + currentPaths + ); + } + return finalOrder; + }, + + getUpdatedPaths: ( + paths: string[], + pathToUpdate: string, + toIndex: number + ): string[] => { + const currentIndex = paths.indexOf(pathToUpdate); + if (currentIndex === -1 || currentIndex === toIndex) return paths; + return moveItemInArray(paths, currentIndex, toIndex); + }, + + moveItemInManualOrder: async ( + order: ManualSortOrder, + item: TAbstractFile, + atIndex: number, + updatedOrderAndSave: ( + updatedOrder: ManualSortOrder + ) => Promise + ) => { + const { getUpdatedPaths } = get(); + + const parentPath = item.parent?.path; + if (!parentPath) return; + + const currentPaths = order[parentPath] ?? []; + if (!currentPaths.length) return; + + const updatedPaths = getUpdatedPaths( + currentPaths, + item.path, + atIndex + ); + + if (areArraysEqual(updatedPaths, currentPaths)) return; + + const updatedOrder = { + ...order, + [parentPath]: updatedPaths, + }; + + await updatedOrderAndSave(updatedOrder); + }, + + updatePathInManualOrder: async ( + order: ManualSortOrder, + saveOrder: (updatedOrder: ManualSortOrder) => Promise, + { parentPath, oldPath, newPath }: UpdatePathInManualOrderPathParams + ) => { + const updatedOrder = { ...order }; + + const orderedPaths = updatedOrder[parentPath] ?? []; + if (!orderedPaths.length) { + updatedOrder[parentPath] = [newPath]; + await saveOrder(updatedOrder); + return; + } + + const index = orderedPaths.indexOf(oldPath); + if (index >= 0) { + orderedPaths[index] = newPath; + } else { + orderedPaths.push(newPath); + } + await saveOrder(updatedOrder); + }, + }); diff --git a/src/store/common/pin.ts b/src/store/common/pin.ts new file mode 100644 index 0000000..7f5d682 --- /dev/null +++ b/src/store/common/pin.ts @@ -0,0 +1,119 @@ +import { TAbstractFile } from "obsidian"; +import { StateCreator } from "zustand"; + +import FolderFileSplitterPlugin from "src/main"; +import { ExplorerStore } from "src/store"; +import { replaceItemInArray, uniq } from "src/utils"; + +import { TagNode } from "../tag"; + +export type PinnedItem = TAbstractFile | TagNode; + +export type PinSlice = { + getPinnedItems: ( + paths: string[], + getter: (path: string) => T | undefined + ) => T[]; + + isPinned: (pinnedPaths: string[], path: string) => boolean; + setPinnedPathsAndSave: ( + key: string, + pluginKey: string, + paths: string[] + ) => Promise; + pinItem: ( + path: string, + paths: string[], + save: (paths: string[]) => Promise + ) => Promise; + unpinItem: ( + path: string, + paths: string[], + save: (paths: string[]) => Promise + ) => Promise; + updatePinnedPath: ( + oldPath: string, + newPath: string, + pinnedPaths: string[], + save: (paths: string[]) => Promise + ) => Promise; + restorePinnedPaths: (key: string, pluginKey: string) => Promise; +}; + +export const createPinSlice = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get) => ({ + getPinnedItems: ( + paths: string[], + getter: (path: string) => T | undefined + ): T[] => { + return uniq(paths).map(getter).filter(Boolean) as T[]; + }, + + isPinned: (pinnedPaths: string[], path: string): boolean => { + return pinnedPaths.includes(path); + }, + + setPinnedPathsAndSave: async ( + key: string, + pluginKey: string, + paths: string[] + ) => { + const { setValueAndSaveInPlugin } = get(); + const uniquePaths = uniq(paths); + await setValueAndSaveInPlugin({ + key, + value: uniquePaths, + pluginKey, + pluginValue: JSON.stringify(uniquePaths), + }); + }, + + pinItem: async ( + path: string, + paths: string[], + save: (paths: string[]) => Promise + ) => { + const { isPinned } = get(); + if (isPinned(paths, path)) return; + const newPaths = [...paths, path]; + await save(newPaths); + }, + unpinItem: async ( + path: string, + paths: string[], + save: (paths: string[]) => Promise + ) => { + const { isPinned } = get(); + if (!isPinned(paths, path)) return; + const newPaths = paths.filter((p) => p !== path); + await save(newPaths); + }, + + updatePinnedPath: async ( + oldPath: string, + newPath: string, + pinnedPaths: string[], + save: (paths: string[]) => Promise + ) => { + const { isPinned } = get(); + if (!isPinned(pinnedPaths, oldPath)) return; + const updatedPaths = replaceItemInArray( + pinnedPaths, + oldPath, + newPath + ); + await save(updatedPaths); + }, + + restorePinnedPaths: async (key: string, pluginKey: string) => { + const { restoreDataFromPlugin } = get(); + await restoreDataFromPlugin({ + pluginKey, + key, + needParse: true, + }); + }, + }); diff --git a/src/store/common/plugin.ts b/src/store/common/plugin.ts new file mode 100644 index 0000000..73b24f7 --- /dev/null +++ b/src/store/common/plugin.ts @@ -0,0 +1,110 @@ +import { StateCreator } from "zustand"; + +import { ExplorerStore } from "src/store"; +import { logError, uniq } from "src/utils"; + +import FolderFileSplitterPlugin from "../../main"; + +type SetValueAndSaveInPluginParams = { + key: string; + value: T; + pluginKey: string; + pluginValue: T | string; +}; +type RestoreDataFromPluginParams = { + pluginKey: string; + key: string; + needParse?: boolean; + transform?: (value: unknown) => unknown; + validate?: (value: unknown) => boolean; +}; + +export type PluginSlice = { + getDataFromPlugin: (key: string) => Promise; + saveDataInPlugin: (oata: Record) => Promise; + + setValueAndSaveInPlugin: ({ + key, + value, + pluginKey, + pluginValue, + }: SetValueAndSaveInPluginParams) => Promise; + restoreDataFromPlugin: ({ + pluginKey, + key, + needParse, + }: RestoreDataFromPluginParams) => Promise; +}; + +export const createPluginSlice = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get, store) => ({ + saveDataInPlugin: async ( + data: Record + ): Promise => { + const previousData = await plugin.loadData(); + await plugin.saveData({ + ...previousData, + ...data, + }); + }, + getDataFromPlugin: async (key: string): Promise => { + try { + const data = await plugin.loadData(); + return data[key]; + } catch (e) { + console.error(e); + return undefined; + } + }, + + setValueAndSaveInPlugin: async ({ + key, + value, + pluginKey, + pluginValue, + }: SetValueAndSaveInPluginParams): Promise => { + const { saveDataInPlugin } = get(); + set({ [key]: value } as Partial); + await saveDataInPlugin({ [pluginKey]: pluginValue }); + }, + restoreDataFromPlugin: async ({ + pluginKey, + key, + needParse = false, + transform, + validate, + }: RestoreDataFromPluginParams) => { + const { getDataFromPlugin } = get(); + const raw = await getDataFromPlugin(pluginKey); + if (!raw) return; + + try { + const parsed = needParse ? JSON.parse(raw) : raw; + const removeDuplicate = Array.isArray(parsed) + ? uniq(parsed) + : parsed; + const final = transform + ? transform(removeDuplicate) + : removeDuplicate; + + if (validate && !validate(final)) return; + + set({ [key]: final } as Partial); + return parsed; + } catch (error) { + logError({ + name: "restoreDataFromPlugin", + error, + data: raw, + params: { + key, + pluginKey, + needParse, + }, + }); + } + }, + }); diff --git a/src/store/common/viewMode.ts b/src/store/common/viewMode.ts new file mode 100644 index 0000000..55c3116 --- /dev/null +++ b/src/store/common/viewMode.ts @@ -0,0 +1,63 @@ +import { StateCreator } from "zustand"; + +import { FFS_VIEW_MODE_KEY } from "src/assets/constants"; +import FolderFileSplitterPlugin from "src/main"; +import { ValueOf } from "src/settings"; +import { ExplorerStore } from "src/store"; + +export const VIEW_MODE = { + ALL: "all", + TAG: "tag", + FOLDER: "folder", +}; +export type ViewMode = ValueOf; +export const DEFAULT_VIEW_MODE: ViewMode = VIEW_MODE.FOLDER; + +export type ViewModeSlice = { + viewMode: ViewMode; + changeViewMode: (mode: ViewMode) => void; + restoreViewMode: () => void; + + changeToTagMode: () => void; + changeToFolderMode: () => void; +}; + +export const createViewModeSlice = + ( + plugin: FolderFileSplitterPlugin + ): StateCreator => + (set, get) => ({ + viewMode: DEFAULT_VIEW_MODE, + + changeViewMode: (mode: ViewMode) => { + const { setValueAndSaveInLocalStorage } = get(); + setValueAndSaveInLocalStorage({ + key: "viewMode", + value: mode, + localStorageKey: FFS_VIEW_MODE_KEY, + localStorageValue: mode, + }); + }, + restoreViewMode: () => { + const { getDataFromLocalStorage } = get(); + const viewMode = getDataFromLocalStorage(FFS_VIEW_MODE_KEY); + if (!viewMode) return; + set({ + viewMode, + }); + }, + + changeToFolderMode: () => { + const { changeViewMode, setFocusedTag } = get(); + changeViewMode(VIEW_MODE.FOLDER); + setFocusedTag(null); + }, + changeToTagMode: () => { + const { + changeViewMode, + setFocusedFileAndSave: setFocusedFolderAndSave, + } = get(); + changeViewMode(VIEW_MODE.TAG); + setFocusedFolderAndSave(null); + }, + }); diff --git a/src/store/file/actions.ts b/src/store/file/actions.ts index 7096bec..5a1107e 100644 --- a/src/store/file/actions.ts +++ b/src/store/file/actions.ts @@ -2,19 +2,29 @@ import { TFile, TFolder } from "obsidian"; import { StateCreator } from "zustand"; import FolderFileSplitterPlugin from "src/main"; -import { isFile } from "src/utils"; +import { getCopyName, getDefaultUntitledName } from "src/utils"; import { ExplorerStore } from ".."; +export const DEFAULT_NEW_FILE_CONTENT = ""; +export const MARKDOWN_FILE_EXTENSION = ".md"; + export interface FileActionsSlice { + getNewFileDefaultName: (folder: TFolder) => string; + getDuplicateFilePath: (file: TFile) => string; + + openFile: (file: TFile, focus?: boolean) => void; + selectFileAndOpen: (file: TFile, focus?: boolean) => void; + readFile: (file: TFile) => Promise; + + createFileAndOpen: (path: string, focus?: boolean) => Promise; createFile: (folder: TFolder) => Promise; duplicateFile: (file: TFile) => Promise; - trashFile: (file: TFile) => Promise; + moveFile: (file: TFile, newPath: string) => Promise; renameFile: (file: TFile, newName: string) => Promise; - openFile: (file: TFile, focus?: boolean) => void; - selectFile: (file: TFile, focus?: boolean) => Promise; - readFile: (file: TFile) => Promise; + + trashFile: (file: TFile) => Promise; } export const createFileActionsSlice = @@ -22,79 +32,62 @@ export const createFileActionsSlice = plugin: FolderFileSplitterPlugin ): StateCreator => (set, get) => ({ + getNewFileDefaultName: (folder: TFolder): string => { + const files = get().getFilesInFolder(folder); + return getDefaultUntitledName(files.map((file) => file.basename)); + }, + + getDuplicateFilePath: (file: TFile): string => { + const { getFilesInFolder, rootFolder } = get(); + const defaultFileName = file.basename; + const files = getFilesInFolder(file.parent || rootFolder); + const copyName = getCopyName( + files.map((f) => f.name), + defaultFileName + ); + return file.path.replace(file.basename, copyName); + }, + openFile: (file: TFile, focus = true): void => { - const leaf = plugin.app.workspace.getLeaf(); - plugin.app.workspace.setActiveLeaf(leaf, { focus }); + const { workspace } = plugin.app; + const { getLeaf, setActiveLeaf } = workspace; + + const leaf = getLeaf(); + setActiveLeaf(leaf, { focus }); leaf.openFile(file, { eState: { focus } }); }, - selectFile: async (file: TFile, focus?: boolean): Promise => { - const { setFocusedFile, openFile } = get(); - await setFocusedFile(file); + selectFileAndOpen: (file: TFile, focus?: boolean): void => { + const { setFocusedFileAndSave, openFile } = get(); + setFocusedFileAndSave(file); openFile(file, focus); }, readFile: async (file: TFile): Promise => { return await plugin.app.vault.read(file); }, - createFile: async (folder: TFolder) => { - const { vault } = plugin.app; - const defaultFileName = "Untitled"; - let newFileName = defaultFileName; - let untitledFilesCount = 0; - folder.children.forEach((child) => { - if (!isFile(child)) return; - - if (child.basename === newFileName) { - untitledFilesCount++; - } else if (child.name.startsWith(defaultFileName)) { - const suffix = child.basename - .replace(defaultFileName, "") - .trim(); - const number = parseInt(suffix, 10); - if (!isNaN(number) && number > untitledFilesCount) { - untitledFilesCount = number; - } - } - }); - - if (untitledFilesCount > 0) { - newFileName = `${defaultFileName} ${untitledFilesCount + 1}`; - } - try { - const newFile = await vault.create( - `${folder.path}/${newFileName}.md`, - "" - ); - get().selectFile(newFile, false); - return newFile; - } catch (e) { - alert(e); - } - }, - duplicateFile: async (file: TFile) => { - const { vault } = plugin.app; - const defaultFileName = file.basename; - const folder = file.parent || plugin.app.vault.getRoot(); - - const newFileName = `${defaultFileName} copy.md`; - if (folder.children.some((child) => child.name === newFileName)) { - alert("文件已存在,请重命名后再试。"); - } - const newFile = await vault.copy( - file, - `${folder.path}/${newFileName}` + createFileAndOpen: async (path: string, focus?: boolean) => { + const file = await plugin.app.vault.create( + path, + DEFAULT_NEW_FILE_CONTENT ); - get().selectFile(newFile, false); + get().selectFileAndOpen(file, focus); + return file; + }, + createFile: async (folder: TFolder) => { + const { createFileAndOpen, getNewFileDefaultName } = get(); + const newFileName = getNewFileDefaultName(folder); + const filePath = `${folder.path}/${newFileName}${MARKDOWN_FILE_EXTENSION}`; + const newFile = await createFileAndOpen(filePath, false); return newFile; }, - trashFile: async (file: TFile) => { - const { setFocusedFile, focusedFile } = get(); - const { app } = plugin; - if (file.path === focusedFile?.path) { - await setFocusedFile(null); - } - await app.fileManager.trashFile(file); + duplicateFile: async (file: TFile) => { + const { selectFileAndOpen, getDuplicateFilePath } = get(); + const newFilePath = getDuplicateFilePath(file); + const newFile = await plugin.app.vault.copy(file, newFilePath); + selectFileAndOpen(newFile, false); + return newFile; }, + moveFile: async (file: TFile, newPath: string) => { await plugin.app.fileManager.renameFile(file, newPath); }, @@ -103,4 +96,13 @@ export const createFileActionsSlice = const newPath = file.path.replace(file.basename, newName); await moveFile(file, newPath); }, + + trashFile: async (file: TFile) => { + const { setFocusedFileAndSave, focusedFile } = get(); + + await plugin.app.fileManager.trashFile(file); + if (file.path === focusedFile?.path) { + setFocusedFileAndSave(null); + } + }, }); diff --git a/src/store/file/focus.ts b/src/store/file/focus.ts index a1b4985..0db0aea 100644 --- a/src/store/file/focus.ts +++ b/src/store/file/focus.ts @@ -5,15 +5,13 @@ import { FFS_FOCUSED_FILE_PATH_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from ".."; -import { VIEW_MODE } from "../common"; export interface FocusedFileSlice { focusedFile: TFile | null; - getVisibleFiles: () => TFile[]; - findFileByPath: (path: string) => TFile | null; - setFocusedFile: (file: TFile | null) => Promise; + setFocusedFileAndSave: (file: TFile | null) => void; restoreLastFocusedFile: () => Promise; + clearFocusedFile: () => Promise; } export const createFocusedFileSlice = @@ -23,29 +21,7 @@ export const createFocusedFileSlice = (set, get) => ({ focusedFile: null, - getVisibleFiles: () => { - const { - focusedFolder, - focusedTag, - getFilesInFolder, - getFilesInTag, - viewMode, - } = get(); - switch (viewMode) { - case VIEW_MODE.ALL: - return plugin.app.vault.getFiles(); - case VIEW_MODE.FOLDER: - return focusedFolder ? getFilesInFolder(focusedFolder) : []; - case VIEW_MODE.TAG: - return focusedTag ? getFilesInTag(focusedTag) : []; - default: - return []; - } - }, - findFileByPath: (path: string): TFile | null => { - return plugin.app.vault.getFileByPath(path); - }, - setFocusedFile: async (file: TFile | null) => { + setFocusedFileAndSave: (file: TFile | null) => { const { setValueAndSaveInLocalStorage } = get(); setValueAndSaveInLocalStorage({ key: "focusedFile", @@ -56,15 +32,22 @@ export const createFocusedFileSlice = }, restoreLastFocusedFile: async () => { - const { findFileByPath, selectFile, getDataFromLocalStorage } = - get(); + const { + findFileByPath, + selectFileAndOpen, + getDataFromLocalStorage, + } = get(); const lastFocusedFilePath = getDataFromLocalStorage( FFS_FOCUSED_FILE_PATH_KEY ); if (!lastFocusedFilePath) return; const file = findFileByPath(lastFocusedFilePath); if (file) { - selectFile(file); + selectFileAndOpen(file); } }, + + clearFocusedFile: async () => { + await get().setFocusedFileAndSave(null); + }, }); diff --git a/src/store/file/manualSort.ts b/src/store/file/manualSort.ts index 2ed2004..aa5fa90 100644 --- a/src/store/file/manualSort.ts +++ b/src/store/file/manualSort.ts @@ -5,31 +5,27 @@ import { FFS_FILE_MANUAL_SORT_ORDER_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from ".."; -import { ManualSortOrder } from "../common"; - -import { FILE_MANUAL_SORT_RULE } from "./sort"; -import { moveItemInArray } from "src/utils"; +import { DEFAULT_MANUAL_SORT_ORDER, ManualSortOrder } from "../common"; export interface ManualSortFileSlice { filesManualSortOrder: ManualSortOrder; - initFilesManualSortOrder: () => Promise; getInitialFilesOrder: () => ManualSortOrder; + getRestoredFilesOrder: () => Promise; + + setFilesManualOrderAndSave: ( + updatedOrder: ManualSortOrder + ) => Promise; + + initFilesManualSortOrder: () => Promise; restoreFilesManualSortOrder: () => Promise; - updateFileManualOrder: ( + updateFilePathInManualOrder: ( parentPath: string, oldPath: string, newPath: string ) => Promise; - changeFilesManualOrder: ( - file: TFile, - atIndex: number - ) => ManualSortOrder | undefined; - changeFilesManualOrderAndSave: ( - file: TFile, - atIndex: number - ) => Promise; - _updateAndSaveFilesOrder: (updatedOrder: ManualSortOrder) => Promise; + moveFileInManualOrder: (file: TFile, atIndex: number) => Promise; + clearFileManualOrderAndSave: () => Promise; } export const createManualSortFileSlice = @@ -40,101 +36,26 @@ export const createManualSortFileSlice = filesManualSortOrder: {}, getInitialFilesOrder: () => { - const { fileSortRule, sortFiles, getFilesInFolder } = get(); - const foldersToInit = plugin.app.vault.getAllFolders(true); - const order: ManualSortOrder = {}; - foldersToInit.forEach((folder) => { - if (folder) { - const files = getFilesInFolder(folder); - if (files.length) { - const sortedFiles = sortFiles(files, fileSortRule); - order[folder.path] = sortedFiles.map( - (file) => file.path - ); - } - } - }); - return order; - }, - initFilesManualSortOrder: async () => { const { - fileSortRule, sortFiles, getFilesInFolder, - setValueAndSaveInPlugin, + foldersWithRoot: foldersToInit, + getInitialOrder, } = get(); - const foldersToInit = plugin.app.vault.getAllFolders(true); - const order: ManualSortOrder = {}; - foldersToInit.forEach((folder) => { - const files = getFilesInFolder(folder); - if (files.length) { - const sortedFiles = sortFiles(files, fileSortRule); - order[folder.path] = sortedFiles.map((file) => file.path); - } - }); - await setValueAndSaveInPlugin({ - key: "filesManualSortOrder", - value: order, - pluginKey: FFS_FILE_MANUAL_SORT_ORDER_KEY, - pluginValue: order, - }); - }, - restoreFilesManualSortOrder: async () => { - const { - getDataFromPlugin, - getInitialFilesOrder, - _updateAndSaveFilesOrder, - } = get(); - const { vault } = plugin.app; - const order = getInitialFilesOrder(); - const previousOrder = - (await getDataFromPlugin( - FFS_FILE_MANUAL_SORT_ORDER_KEY - )) ?? {}; - Object.keys(previousOrder).forEach((path) => { - if (!vault.getFolderByPath(path)) return; - const paths = previousOrder[path].filter((path) => - Boolean(vault.getFileByPath(path)) - ); - if (paths.length > 0) { - order[path] = paths; - } - }); - await _updateAndSaveFilesOrder(order); - }, - changeFilesManualOrder: (file: TFile, atIndex: number) => { - const { filesManualSortOrder } = get(); - const parentPath = file.parent?.path; - if (!parentPath) return; - - const initialOrder = filesManualSortOrder[parentPath] ?? []; - const currentIndex = initialOrder.indexOf(file.path); - if (currentIndex === atIndex) { - return filesManualSortOrder; - } - const newOrder = moveItemInArray( - initialOrder, - currentIndex, - atIndex + const order: ManualSortOrder = getInitialOrder( + foldersToInit, + getFilesInFolder, + sortFiles ); - const updatedOrder = { - ...filesManualSortOrder, - [parentPath]: newOrder, - }; - set({ - filesManualSortOrder: updatedOrder, - fileSortRule: FILE_MANUAL_SORT_RULE, - }); - return updatedOrder; + + return order; }, - changeFilesManualOrderAndSave: async (file: TFile, atIndex: number) => { - const { saveDataInPlugin, changeFilesManualOrder } = get(); - const updatedOrder = changeFilesManualOrder(file, atIndex); - await saveDataInPlugin({ - [FFS_FILE_MANUAL_SORT_ORDER_KEY]: updatedOrder, - }); + getRestoredFilesOrder: async () => { + const { getRestoredOrder } = get(); + return await getRestoredOrder(FFS_FILE_MANUAL_SORT_ORDER_KEY); }, - _updateAndSaveFilesOrder: async (updatedOrder: ManualSortOrder) => { + + setFilesManualOrderAndSave: async (updatedOrder: ManualSortOrder) => { const { setValueAndSaveInPlugin } = get(); await setValueAndSaveInPlugin({ key: "filesManualSortOrder", @@ -143,24 +64,63 @@ export const createManualSortFileSlice = pluginValue: updatedOrder, }); }, - updateFileManualOrder: async ( + + initFilesManualSortOrder: async () => { + const { getInitialFilesOrder, setFilesManualOrderAndSave } = get(); + const order: ManualSortOrder = getInitialFilesOrder(); + await setFilesManualOrderAndSave(order); + }, + restoreFilesManualSortOrder: async () => { + const { + getRestoredFilesOrder, + getInitialFilesOrder, + resolveValidManualSortOrder, + isFilePathValid, + setFilesManualOrderAndSave, + } = get(); + const restoredOrder = await getRestoredFilesOrder(); + const initialOrder = getInitialFilesOrder(); + const finalOrder = resolveValidManualSortOrder( + restoredOrder, + initialOrder, + isFilePathValid + ); + + await setFilesManualOrderAndSave(finalOrder); + }, + + moveFileInManualOrder: async (file: TFile, atIndex: number) => { + const { + filesManualSortOrder, + setFilesManualOrderAndSave, + moveItemInManualOrder, + } = get(); + await moveItemInManualOrder( + filesManualSortOrder, + file, + atIndex, + setFilesManualOrderAndSave + ); + }, + + updateFilePathInManualOrder: async ( parentPath: string, oldPath: string, newPath: string ) => { - const { filesManualSortOrder: order, _updateAndSaveFilesOrder } = - get(); - const orderedPaths = [...(order[parentPath] ?? [])]; - if (!orderedPaths.length) return; - const index = orderedPaths.indexOf(oldPath); - if (index >= 0) { - orderedPaths[index] = newPath; - } else { - orderedPaths.push(newPath); - } - _updateAndSaveFilesOrder({ - ...order, - [parentPath]: orderedPaths, + const { + filesManualSortOrder: order, + setFilesManualOrderAndSave, + updatePathInManualOrder, + } = get(); + await updatePathInManualOrder(order, setFilesManualOrderAndSave, { + parentPath, + oldPath, + newPath, }); }, + + clearFileManualOrderAndSave: async () => { + await get().setFilesManualOrderAndSave(DEFAULT_MANUAL_SORT_ORDER); + }, }); diff --git a/src/store/file/pin.ts b/src/store/file/pin.ts index 1b558f3..ab064c5 100644 --- a/src/store/file/pin.ts +++ b/src/store/file/pin.ts @@ -3,7 +3,6 @@ import { StateCreator } from "zustand"; import { FFS_PINNED_FILE_PATHS_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; -import { removeItemFromArray, replaceItemInArray, uniq } from "src/utils"; import { ExplorerStore } from ".."; @@ -32,10 +31,10 @@ export const createPinnedFileSlice = pinnedFilePaths: [], getPinnedFiles: () => { - const { files, pinnedFilePaths } = get(); - return uniq(pinnedFilePaths) - .map((path) => files.find((file) => file.path === path)) - .filter(Boolean) as TFile[]; + const { files, pinnedFilePaths, getPinnedItems } = get(); + return getPinnedItems(pinnedFilePaths, (path) => + files.find((file) => file.path === path) + ); }, getDisplayedPinnedFiles: () => { const { getVisibleFiles, getPinnedFiles } = get(); @@ -47,48 +46,54 @@ export const createPinnedFileSlice = }, isFilePinned: (file: TFile) => { - const { pinnedFilePaths } = get(); - return pinnedFilePaths.includes(file.path); + const { pinnedFilePaths, isPinned } = get(); + return isPinned(pinnedFilePaths, file.path); }, setPinnedFilePathsAndSave: async (filePaths: string[]) => { - const { setValueAndSaveInPlugin } = get(); - const paths = uniq(filePaths); - await setValueAndSaveInPlugin({ - key: "pinnedFilePaths", - value: paths, - pluginKey: FFS_PINNED_FILE_PATHS_KEY, - pluginValue: JSON.stringify(paths), - }); + await get().setPinnedPathsAndSave( + "pinnedFilePaths", + FFS_PINNED_FILE_PATHS_KEY, + filePaths + ); }, pinFile: async (file: TFile) => { - const { pinnedFilePaths, setPinnedFilePathsAndSave } = get(); - const filePaths = [...pinnedFilePaths, file.path]; - await setPinnedFilePathsAndSave(filePaths); + const { pinnedFilePaths, setPinnedFilePathsAndSave, pinItem } = + get(); + await pinItem( + file.path, + pinnedFilePaths, + setPinnedFilePathsAndSave + ); }, unpinFile: async (file: TFile) => { - const { pinnedFilePaths, setPinnedFilePathsAndSave } = get(); - const filePaths = removeItemFromArray(pinnedFilePaths, file.path); - await setPinnedFilePathsAndSave(filePaths); + const { pinnedFilePaths, setPinnedFilePathsAndSave, unpinItem } = + get(); + await unpinItem( + file.path, + pinnedFilePaths, + setPinnedFilePathsAndSave + ); }, updatePinnedFilePath: async (oldPath: string, newPath: string) => { - const { pinnedFilePaths, setPinnedFilePathsAndSave } = get(); - if (!pinnedFilePaths.includes(oldPath)) return; - const updatedPaths = replaceItemInArray( + const { pinnedFilePaths, + setPinnedFilePathsAndSave, + updatePinnedPath, + } = get(); + await updatePinnedPath( oldPath, - newPath + newPath, + pinnedFilePaths, + setPinnedFilePathsAndSave ); - await setPinnedFilePathsAndSave(updatedPaths); }, restorePinnedFiles: async () => { - const { restoreDataFromPlugin } = get(); - await restoreDataFromPlugin({ - pluginKey: FFS_PINNED_FILE_PATHS_KEY, - key: "pinnedFilePaths", - needParse: true, - }); + await get().restorePinnedPaths( + "pinnedFilePaths", + FFS_PINNED_FILE_PATHS_KEY + ); }, }); diff --git a/src/store/file/sort.ts b/src/store/file/sort.ts index 8960874..3d33b8a 100644 --- a/src/store/file/sort.ts +++ b/src/store/file/sort.ts @@ -17,13 +17,25 @@ export type FileSortRule = export const DEFAULT_FILE_SORT_RULE: FileSortRule = "FileNameAscending"; export const FILE_MANUAL_SORT_RULE: FileSortRule = "FileManualOrder"; +const FILES_SORTERS: Record number> = { + FileNameAscending: (a, b) => a.name.localeCompare(b.name), + FileNameDescending: (a, b) => b.name.localeCompare(a.name), + FileCreatedTimeAscending: (a, b) => a.stat.ctime - b.stat.ctime, + FileCreatedTimeDescending: (a, b) => b.stat.ctime - a.stat.ctime, + FileModifiedTimeAscending: (a, b) => a.stat.mtime - b.stat.mtime, + FileModifiedTimeDescending: (a, b) => b.stat.mtime - a.stat.mtime, + FileManualOrder: () => 0, // special case +}; + export interface SortFileSlice { fileSortRule: FileSortRule; - isFilesInAscendingOrder: () => boolean; - sortFiles: (files: TFile[], rule: FileSortRule) => TFile[]; + filesSortRulesGroup: FileSortRule[][]; + + sortFiles: (files: TFile[]) => TFile[]; restoreFileSortRule: () => Promise; changeFileSortRule: (rule: FileSortRule) => Promise; + changeFileSortRuleAndUpdateOrder: (rule: FileSortRule) => Promise; } export const createSortFileSlice = @@ -33,43 +45,39 @@ export const createSortFileSlice = (set, get) => ({ fileSortRule: DEFAULT_FILE_SORT_RULE, - isFilesInAscendingOrder: (): boolean => { - const { fileSortRule } = get(); - return fileSortRule.contains("Ascending"); + get filesSortRulesGroup(): FileSortRule[][] { + return [ + ["FileNameAscending", "FileNameDescending"], + ["FileModifiedTimeAscending", "FileModifiedTimeDescending"], + ["FileCreatedTimeAscending", "FileCreatedTimeDescending"], + ["FileManualOrder"], + ]; }, - sortFiles: (files: TFile[], rule: FileSortRule): TFile[] => { - const { filesManualSortOrder } = get(); + + sortFiles: (files: TFile[]): TFile[] => { + const { filesManualSortOrder, fileSortRule: rule } = get(); if (files.length === 0) return files; - const parentPath = files[0].parent?.path; - const filePaths = parentPath - ? filesManualSortOrder[parentPath] - : []; - switch (rule) { - case "FileNameAscending": - return files.sort((a, b) => a.name.localeCompare(b.name)); - case "FileNameDescending": - return files.sort((a, b) => b.name.localeCompare(a.name)); - case "FileCreatedTimeAscending": - return files.sort((a, b) => a.stat.ctime - b.stat.ctime); - case "FileCreatedTimeDescending": - return files.sort((a, b) => b.stat.ctime - a.stat.ctime); - case "FileModifiedTimeAscending": - return files.sort((a, b) => a.stat.mtime - b.stat.mtime); - case "FileModifiedTimeDescending": - return files.sort((a, b) => b.stat.mtime - a.stat.mtime); - case "FileManualOrder": - if (!parentPath || !filePaths || !filePaths.length) - return files; - return filePaths - .map((path) => files.find((f) => f.path === path)) - .concat( - files.filter((f) => !filePaths.includes(f.path)) - ) - .filter(Boolean) as TFile[]; - default: - return files; + if (rule === "FileManualOrder") { + const parentPath = files[0].parent?.path; + if (!parentPath) return files; + + const sortedFilePaths = filesManualSortOrder[parentPath] ?? []; + if (!sortedFilePaths.length) return files; + + return [...files].sort((a, b) => { + const indexA = sortedFilePaths.indexOf(a.path); + const indexB = sortedFilePaths.indexOf(b.path); + + if (indexA === -1 && indexB === -1) return 0; + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); } + + const sorter = FILES_SORTERS[rule]; + return [...files].sort(sorter); }, changeFileSortRule: async (rule: FileSortRule) => { const { setValueAndSaveInPlugin } = get(); @@ -80,6 +88,22 @@ export const createSortFileSlice = pluginValue: rule, }); }, + changeFileSortRuleAndUpdateOrder: async (rule: FileSortRule) => { + const { + fileSortRule, + changeFileSortRule, + initFilesManualSortOrder, + clearFileManualOrderAndSave, + } = get(); + if (rule !== fileSortRule) { + await changeFileSortRule(rule); + if (rule === FILE_MANUAL_SORT_RULE) { + await initFilesManualSortOrder(); + } else { + await clearFileManualOrderAndSave(); + } + } + }, restoreFileSortRule: async () => { const { restoreFilesManualSortOrder, restoreDataFromPlugin } = get(); diff --git a/src/store/file/structure.ts b/src/store/file/structure.ts index 248aab7..5812e31 100644 --- a/src/store/file/structure.ts +++ b/src/store/file/structure.ts @@ -4,9 +4,15 @@ import { StateCreator } from "zustand"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from ".."; +import { VIEW_MODE } from "../common"; export interface FileStructureSlice { files: TFile[]; + getMarkdownFiles: () => TFile[]; + getVisibleFiles: () => TFile[]; + + findFileByPath: (path: string) => TFile | null; + isFilePathValid: (path: string) => boolean; } export const createFileStructureSlice = @@ -15,4 +21,35 @@ export const createFileStructureSlice = ): StateCreator => (set, get) => ({ files: plugin.app.vault.getFiles() || [], + + getMarkdownFiles: () => { + return plugin.app.vault.getMarkdownFiles(); + }, + + getVisibleFiles: () => { + const { + focusedFolder, + focusedTag, + getFilesInFolder, + getFilesInTag, + viewMode, + } = get(); + switch (viewMode) { + case VIEW_MODE.ALL: + return plugin.app.vault.getFiles(); + case VIEW_MODE.FOLDER: + return focusedFolder ? getFilesInFolder(focusedFolder) : []; + case VIEW_MODE.TAG: + return focusedTag ? getFilesInTag(focusedTag) : []; + default: + return []; + } + }, + + findFileByPath: (path: string): TFile | null => { + return plugin.app.vault.getFileByPath(path); + }, + isFilePathValid: (path: string) => { + return Boolean(get().findFileByPath(path)); + }, }); diff --git a/src/store/folder/actions.ts b/src/store/folder/actions.ts index ec92d1b..1596553 100644 --- a/src/store/folder/actions.ts +++ b/src/store/folder/actions.ts @@ -2,18 +2,22 @@ import { TFolder } from "obsidian"; import { StateCreator } from "zustand"; import FolderFileSplitterPlugin from "src/main"; -import { isFolder } from "src/utils"; +import { getDefaultUntitledName } from "src/utils"; import { ExplorerStore } from ".."; export interface FolderActionsSlice { latestCreatedFolder: TFolder | null; latestFolderCreatedTime: number | null; - _createFolder: (path: string) => Promise; + + getNewFolderDefaultName: (parentFolder: TFolder) => string; + createNewFolder: (parentFolder: TFolder) => Promise; - trashFolder: (folder: TFolder) => Promise; + moveFolder: (folder: TFolder, newPath: string) => Promise; renameFolder: (folder: TFolder, newName: string) => Promise; + + trashFolder: (folder: TFolder) => Promise; } export const createFolderActionsSlice = @@ -24,57 +28,36 @@ export const createFolderActionsSlice = latestCreatedFolder: null, latestFolderCreatedTime: null, - _createFolder: async (path: string): Promise => { - const newFolder = await plugin.app.vault.createFolder(path); + getNewFolderDefaultName: (parentFolder: TFolder): string => { + const subfolders = get().getSubFolders(parentFolder); + return getDefaultUntitledName( + subfolders.map((folder) => folder.name) + ); + }, + + createNewFolder: async ( + parentFolder: TFolder + ): Promise => { + const { getNewFolderDefaultName } = get(); + + const newFolderName = getNewFolderDefaultName(parentFolder); + const newFolder = await plugin.app.vault.createFolder( + `${parentFolder.path}/${newFolderName}` + ); set({ latestCreatedFolder: newFolder, latestFolderCreatedTime: Date.now(), }); return newFolder; }, - createNewFolder: async ( - parentFolder: TFolder - ): Promise => { - const { rootFolder, _createFolder } = get(); - if (!rootFolder) return; - - const baseFolderName = "Untitled"; - let newFolderName = baseFolderName; - let untitledFoldersCount = 0; - - parentFolder.children.forEach((child) => { - if (!isFolder(child)) return; - - if (child.name === newFolderName) { - untitledFoldersCount++; - } else if (child.name.startsWith(baseFolderName)) { - const suffix = child.name - .replace(baseFolderName, "") - .trim(); - const number = parseInt(suffix, 10); - if (!isNaN(number) && number > untitledFoldersCount) { - untitledFoldersCount = number; - } - } - }); - - if (untitledFoldersCount > 0) { - newFolderName = `${baseFolderName} ${untitledFoldersCount + 1}`; - } - - const newFolder = await _createFolder( - `${parentFolder.path}/${newFolderName}` - ); - return newFolder; - }, trashFolder: async (folder: TFolder) => { - const { focusedFolder, setFocusedFolder } = get(); - const { app } = plugin; - const focusedFolderPaths = focusedFolder?.path.split("/") ?? []; - if (focusedFolderPaths.includes(folder.path)) { - setFocusedFolder(null); + const { focusedFolder, isAnscestorOf, setFocusedFolderAndSave } = + get(); + + if (focusedFolder && isAnscestorOf(folder, focusedFolder)) { + setFocusedFolderAndSave(null); } - await app.fileManager.trashFile(folder); + await plugin.app.fileManager.trashFile(folder); }, moveFolder: async (folder: TFolder, newPath: string) => { diff --git a/src/store/folder/focus.ts b/src/store/folder/focus.ts index 88e0fcb..1ef0a1d 100644 --- a/src/store/folder/focus.ts +++ b/src/store/folder/focus.ts @@ -4,18 +4,21 @@ import { StateCreator } from "zustand"; import { FFS_FOCUSED_FOLDER_PATH_KEY } from "src/assets/constants"; import { NOTIFICATION_MESSAGE_COPY } from "src/locales/message"; import FolderFileSplitterPlugin from "src/main"; -import { FOLDER_NOTE_LOCATION, FOLDER_NOTE_MISSING_BEHAVIOR } from "src/settings"; +import { FOLDER_NOTE_MISSING_BEHAVIOR } from "src/settings"; import { ExplorerStore } from ".."; +import { VIEW_MODE } from "../common"; export interface FocusedFolderSlice { + focusedFolder: TFolder | null; - focusedFolder: TFolder | null; + setFocusedFolderAndSave: (folder: TFolder | null) => void; + changeFocusedFolder: (folder: TFolder | null) => Promise; + restoreLastFocusedFolder: () => Promise; - _setFocusedFolder: (folder: TFolder | null) => void; - setFocusedFolder: (folder: TFolder | null) => Promise; - restoreLastFocusedFolder: () => Promise; + openFolderNote: (folder: TFolder) => Promise; + handleMissingFolderNote: (folderNotePath: string) => Promise; } export const createFocusedFolderSlice = @@ -25,97 +28,86 @@ export const createFocusedFolderSlice = (set, get) => ({ focusedFolder: null, - _setFocusedFolder: (folder: TFolder | null) => - set({ - focusedFolder: folder, - }), - setFocusedFolder: async (folder: TFolder | null) => { + setFocusedFolderAndSave: (folder: TFolder | null) => { + const { setValueAndSaveInLocalStorage } = get(); + setValueAndSaveInLocalStorage({ + key: "focusedFolder", + value: folder, + localStorageKey: FFS_FOCUSED_FOLDER_PATH_KEY, + localStorageValue: folder ? folder.path : "", + }); + }, + + changeFocusedFolder: async (folder: TFolder | null) => { const { - _setFocusedFolder, focusedFile, - setFocusedFile, - saveDataInLocalStorage, - removeDataFromLocalStorage, - selectFile, - setFocusedTag, + setFocusedFolderAndSave, + changeToFolderMode, + openFolderNote, + clearFocusedFile, + viewMode, } = get(); - _setFocusedFolder(folder); - if (!folder) { - removeDataFromLocalStorage(FFS_FOCUSED_FOLDER_PATH_KEY); - } else { - setFocusedTag(null); - saveDataInLocalStorage( - FFS_FOCUSED_FOLDER_PATH_KEY, - folder.path - ); - - const { settings, language } = plugin; - const { - autoOpenFolderNote, - folderNoteLocation, - customFolderNotePath, - folderNoteMissingBehavior, - } = settings; - const { IGNORE, WARN, CREATE } = FOLDER_NOTE_MISSING_BEHAVIOR; - if (autoOpenFolderNote) { - let folderNotePath = ""; - if ( - [ - FOLDER_NOTE_LOCATION.INDEX_FILE, - FOLDER_NOTE_LOCATION.UNDERSCORE_FILE, - ].includes(folderNoteLocation) - ) { - folderNotePath = `${folder?.path}/${folderNoteLocation}`; - } else if ( - folderNoteLocation === - FOLDER_NOTE_LOCATION.FOLDER_NAME_FILE - ) { - folderNotePath = `${folder?.path}/${folder?.name}.md`; - } else { - folderNotePath = customFolderNotePath.replace( - "{folder}", - folder?.name || "" - ); - } - const file = plugin.app.vault.getFileByPath(folderNotePath); - if (file) { - await selectFile(file); - } else { - if (folderNoteMissingBehavior === IGNORE) return; - if (folderNoteMissingBehavior === WARN) { - new Notice( - `${NOTIFICATION_MESSAGE_COPY.folderNoteMissing[language]}'${folderNotePath}'` - ); - } else if (folderNoteMissingBehavior === CREATE) { - const newFile = await plugin.app.vault.create( - folderNotePath, - "" - ); - await selectFile(newFile); - } - } - } else if (focusedFile?.parent?.path !== folder?.path) { - await setFocusedFile(null); + setFocusedFolderAndSave(folder); + if (folder) { + if (viewMode !== VIEW_MODE.FOLDER) { + changeToFolderMode(); } + if (plugin.settings.autoOpenFolderNote) { + await openFolderNote(folder); + } + } + // TODO: Question? + if (focusedFile?.parent?.path !== folder?.path) { + await clearFocusedFile(); } }, - restoreLastFocusedFolder: async () => { - const { getDataFromLocalStorage } = get(); - const lastFocusedFolderPath = getDataFromLocalStorage( - FFS_FOCUSED_FOLDER_PATH_KEY - ); - const { rootFolder, _setFocusedFolder: _setFocusedFolder } = get(); - if (!lastFocusedFolderPath) return; - if (lastFocusedFolderPath !== "/") { - const folder = plugin.app.vault.getFolderByPath( - lastFocusedFolderPath - ); - if (folder) { - _setFocusedFolder(folder); - } - } else if (rootFolder) { - _setFocusedFolder(rootFolder); + + openFolderNote: async (folder: TFolder) => { + const { + selectFileAndOpen, + getFolderNotePath, + findFileByPath, + handleMissingFolderNote, + } = get(); + + const folderNotePath = getFolderNotePath(folder); + const folderNote = findFileByPath(folderNotePath); + if (folderNote) { + selectFileAndOpen(folderNote); + } else { + await handleMissingFolderNote(folderNotePath); } }, + + handleMissingFolderNote: async (folderNotePath: string) => { + const { createFileAndOpen } = get(); + const { settings, language } = plugin; + const { folderNoteMissingBehavior: behavior } = settings; + const { WARN, CREATE } = FOLDER_NOTE_MISSING_BEHAVIOR; + const { folderNoteMissing } = NOTIFICATION_MESSAGE_COPY; + + if (behavior === WARN) { + new Notice(`${folderNoteMissing[language]}'${folderNotePath}'`); + return; + } + if (behavior === CREATE) { + await createFileAndOpen(folderNotePath); + } + }, + + restoreLastFocusedFolder: async () => { + const { + findFolderByPath, + rootFolder, + restoreDataFromLocalStorage, + } = get(); + await restoreDataFromLocalStorage({ + localStorageKey: FFS_FOCUSED_FOLDER_PATH_KEY, + key: "focusedFolder", + transform: (path: string) => + path === "/" ? rootFolder : findFolderByPath(path), + validate: (folder) => Boolean(folder), + }); + }, }); diff --git a/src/store/folder/manualSort.ts b/src/store/folder/manualSort.ts index 724c019..c0aea60 100644 --- a/src/store/folder/manualSort.ts +++ b/src/store/folder/manualSort.ts @@ -5,31 +5,29 @@ import { FFS_FOLDER_MANUAL_SORT_ORDER_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from ".."; -import { ManualSortOrder } from "../common"; -import { moveItemInArray } from "src/utils"; +import { DEFAULT_MANUAL_SORT_ORDER, ManualSortOrder } from "../common"; export interface ManualSortFolderSlice { foldersManualSortOrder: ManualSortOrder; - initFoldersManualSortOrder: () => Promise; getInitialFoldersOrder: () => ManualSortOrder; + getRestoredFoldersOrder: () => Promise; + + initFoldersManualSortOrder: () => Promise; restoreFoldersManualSortOrder: () => Promise; - changeFoldersManualOrder: ( - folder: TFolder, - atIndex: number - ) => ManualSortOrder | undefined; - changeFoldersManualOrderAndSave: ( + moveFolderInManualOrder: ( folder: TFolder, atIndex: number ) => Promise; - _updateAndSaveFoldersOrder: ( + setFoldersManualOrderAndSave: ( updatedOrder: ManualSortOrder ) => Promise; - _updateFolderManualOrder: ( + updateFolderPathInManualOrder: ( parentPath: string, oldPath: string, newPath: string ) => Promise; + clearFoldersManualOrderAndSave: () => Promise; } export const createManualSortFolderSlice = @@ -40,114 +38,26 @@ export const createManualSortFolderSlice = foldersManualSortOrder: {}, getInitialFoldersOrder: () => { - const { folderSortRule, getSubFolders, sortFolders } = get(); - const foldersToInit = plugin.app.vault.getAllFolders(true); - const order: ManualSortOrder = {}; - foldersToInit.forEach((folder) => { - if (folder) { - const folders = getSubFolders(folder); - if (folders.length) { - const sortedFolders = sortFolders( - folders, - folderSortRule, - plugin.settings.includeSubfolderFiles - ); - order[folder.path] = sortedFolders.map( - (folder) => folder.path - ); - } - } - }); - return order; - }, - - initFoldersManualSortOrder: async () => { const { - folderSortRule, getSubFolders, sortFolders, - setValueAndSaveInPlugin, + foldersWithRoot: foldersToInit, + getInitialOrder, } = get(); - const foldersToInit = plugin.app.vault.getAllFolders(true); - const order: ManualSortOrder = {}; - foldersToInit.forEach((folder) => { - const folders = getSubFolders(folder); - if (folders.length) { - const sortedFolders = sortFolders( - folders, - folderSortRule, - plugin.settings.includeSubfolderFiles - ); - order[folder.path] = sortedFolders.map( - (folder) => folder.path - ); - } - }); - await setValueAndSaveInPlugin({ - key: "foldersManualSortOrder", - value: order, - pluginKey: FFS_FOLDER_MANUAL_SORT_ORDER_KEY, - pluginValue: order, - }); - }, - restoreFoldersManualSortOrder: async () => { - const { - getDataFromPlugin, - getInitialFoldersOrder, - _updateAndSaveFoldersOrder, - } = get(); - const { vault } = plugin.app; - const order = getInitialFoldersOrder(); - const previousOrder = - (await getDataFromPlugin( - FFS_FOLDER_MANUAL_SORT_ORDER_KEY - )) ?? {}; - Object.keys(previousOrder).forEach((parentFolderPath) => { - if (!vault.getFolderByPath(parentFolderPath)) return; - const paths = previousOrder[parentFolderPath].filter((p) => - Boolean(vault.getFolderByPath(p)) - ); - if (paths.length > 0) { - order[parentFolderPath] = paths; - } - }); - await _updateAndSaveFoldersOrder(order); - }, - changeFoldersManualOrder: (folder: TFolder, atIndex: number) => { - const { foldersManualSortOrder } = get(); - const parentPath = folder.parent?.path; - if (!parentPath) return; - - const initialOrder = foldersManualSortOrder[parentPath] ?? []; - const currentIndex = initialOrder.indexOf(folder.path); - if (currentIndex === atIndex) { - return foldersManualSortOrder; - } - const newOrder = moveItemInArray( - initialOrder, - currentIndex, - atIndex + const order: ManualSortOrder = getInitialOrder( + foldersToInit, + getSubFolders, + sortFolders ); - const updatedOrder = { - ...foldersManualSortOrder, - [parentPath]: newOrder, - }; - set({ - foldersManualSortOrder: updatedOrder, - }); - return updatedOrder; + + return order; }, - changeFoldersManualOrderAndSave: async ( - folder: TFolder, - atIndex: number - ) => { - const { saveDataInPlugin, changeFoldersManualOrder } = get(); - const updatedOrder = changeFoldersManualOrder(folder, atIndex); - await saveDataInPlugin({ - [FFS_FOLDER_MANUAL_SORT_ORDER_KEY]: updatedOrder, - }); + getRestoredFoldersOrder: async () => { + const { getRestoredOrder } = get(); + return await getRestoredOrder(FFS_FOLDER_MANUAL_SORT_ORDER_KEY); }, - _updateAndSaveFoldersOrder: async (updatedOrder: ManualSortOrder) => { + + setFoldersManualOrderAndSave: async (updatedOrder: ManualSortOrder) => { const { setValueAndSaveInPlugin } = get(); await setValueAndSaveInPlugin({ key: "foldersManualSortOrder", @@ -156,30 +66,62 @@ export const createManualSortFolderSlice = pluginValue: updatedOrder, }); }, - _updateFolderManualOrder: async ( + + initFoldersManualSortOrder: async () => { + const { getInitialFoldersOrder, setFoldersManualOrderAndSave } = + get(); + const order: ManualSortOrder = getInitialFoldersOrder(); + await setFoldersManualOrderAndSave(order); + }, + restoreFoldersManualSortOrder: async () => { + const { + getRestoredFoldersOrder, + getInitialFoldersOrder, + resolveValidManualSortOrder, + isFolderPathValid, + setFoldersManualOrderAndSave, + } = get(); + const restoredOrder = await getRestoredFoldersOrder(); + const initialOrder = getInitialFoldersOrder(); + const finalOrder: ManualSortOrder = resolveValidManualSortOrder( + restoredOrder, + initialOrder, + isFolderPathValid + ); + + await setFoldersManualOrderAndSave(finalOrder); + }, + moveFolderInManualOrder: async (folder: TFolder, atIndex: number) => { + const { + foldersManualSortOrder, + setFoldersManualOrderAndSave, + moveItemInManualOrder, + } = get(); + await moveItemInManualOrder( + foldersManualSortOrder, + folder, + atIndex, + setFoldersManualOrderAndSave + ); + }, + updateFolderPathInManualOrder: async ( parentPath: string, oldPath: string, newPath: string ) => { const { foldersManualSortOrder: order, - _updateAndSaveFoldersOrder, + setFoldersManualOrderAndSave, + updatePathInManualOrder, } = get(); - const orderedPaths = [...(order[parentPath] ?? [])]; - if (!orderedPaths.length) return; - const updatedOrder = { ...order }; - const index = orderedPaths.indexOf(oldPath); - if (index >= 0) { - orderedPaths[index] = newPath; - } else { - orderedPaths.push(newPath); - } - updatedOrder[parentPath] = orderedPaths; - const childPaths = updatedOrder[oldPath]; - if (childPaths) { - delete updatedOrder[oldPath]; - updatedOrder[newPath] = childPaths; - } - _updateAndSaveFoldersOrder(updatedOrder); + await updatePathInManualOrder(order, setFoldersManualOrderAndSave, { + parentPath, + oldPath, + newPath, + }); + }, + + clearFoldersManualOrderAndSave: async () => { + await get().setFoldersManualOrderAndSave(DEFAULT_MANUAL_SORT_ORDER); }, }); diff --git a/src/store/folder/pin.ts b/src/store/folder/pin.ts index 7a42be3..3071727 100644 --- a/src/store/folder/pin.ts +++ b/src/store/folder/pin.ts @@ -3,7 +3,6 @@ import { StateCreator } from "zustand"; import { FFS_PINNED_FOLDER_PATHS_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; -import { removeItemFromArray, replaceItemInArray, uniq } from "src/utils"; import { ExplorerStore } from ".."; @@ -32,10 +31,10 @@ export const createPinnedFolderSlice = pinnedFolderPaths: [], getPinnedFolders: () => { - const { folders, pinnedFolderPaths } = get(); - return uniq(pinnedFolderPaths) - .map((path) => folders.find((folder) => folder.path === path)) - .filter(Boolean) as TFolder[]; + const { folders, pinnedFolderPaths, getPinnedItems } = get(); + return getPinnedItems(pinnedFolderPaths, (path) => + folders.find((folder) => folder.path === path) + ); }, getDisplayedPinnedFolders: () => { const { showFolderView } = plugin.settings; @@ -45,53 +44,57 @@ export const createPinnedFolderSlice = }, isFolderPinned: (folder: TFolder) => { - const { pinnedFolderPaths } = get(); - return pinnedFolderPaths.includes(folder.path); + const { pinnedFolderPaths, isPinned } = get(); + return isPinned(pinnedFolderPaths, folder.path); }, setPinnedFolderPathsAndSave: async (folderPaths: string[]) => { - const { setValueAndSaveInPlugin } = get(); - const paths = uniq(folderPaths); - await setValueAndSaveInPlugin({ - key: "pinnedFolderPaths", - value: paths, - pluginKey: FFS_PINNED_FOLDER_PATHS_KEY, - pluginValue: JSON.stringify(paths), - }); + await get().setPinnedPathsAndSave( + "pinnedFolderPaths", + FFS_PINNED_FOLDER_PATHS_KEY, + folderPaths + ); }, pinFolder: async (folder: TFolder) => { - const { pinnedFolderPaths, setPinnedFolderPathsAndSave } = get(); - if (pinnedFolderPaths.includes(folder.path)) return; - const folderPaths = [...pinnedFolderPaths, folder.path]; - await setPinnedFolderPathsAndSave(folderPaths); + const { pinnedFolderPaths, setPinnedFolderPathsAndSave, pinItem } = + get(); + await pinItem( + folder.path, + pinnedFolderPaths, + setPinnedFolderPathsAndSave + ); }, unpinFolder: async (folder: TFolder) => { - const { pinnedFolderPaths, setPinnedFolderPathsAndSave } = get(); - if (!pinnedFolderPaths.includes(folder.path)) return; - const folderPaths = removeItemFromArray( + const { pinnedFolderPaths, - folder.path + setPinnedFolderPathsAndSave, + unpinItem, + } = get(); + await unpinItem( + folder.path, + pinnedFolderPaths, + setPinnedFolderPathsAndSave ); - await setPinnedFolderPathsAndSave(folderPaths); }, updatePinnedFolderPath: async (oldPath: string, newPath: string) => { - const { pinnedFolderPaths, setPinnedFolderPathsAndSave } = get(); - if (!pinnedFolderPaths.includes(oldPath)) return; - const updatedPaths = replaceItemInArray( + const { pinnedFolderPaths, + setPinnedFolderPathsAndSave, + updatePinnedPath, + } = get(); + await updatePinnedPath( oldPath, - newPath + newPath, + pinnedFolderPaths, + setPinnedFolderPathsAndSave ); - await setPinnedFolderPathsAndSave(updatedPaths); }, restorePinnedFolders: async () => { - const { restoreDataFromPlugin } = get(); - await restoreDataFromPlugin({ - pluginKey: FFS_PINNED_FOLDER_PATHS_KEY, - key: "pinnedFolderPaths", - needParse: true, - }); + await get().restorePinnedPaths( + "pinnedFolderPaths", + FFS_PINNED_FOLDER_PATHS_KEY + ); }, }); diff --git a/src/store/folder/sort.ts b/src/store/folder/sort.ts index a2ac8de..2b903a0 100644 --- a/src/store/folder/sort.ts +++ b/src/store/folder/sort.ts @@ -15,16 +15,27 @@ export type FolderSortRule = export const DEFAULT_FOLDER_SORT_RULE: FolderSortRule = "FolderNameAscending"; export const FOLDER_MANUAL_SORT_RULE: FolderSortRule = "FolderManualOrder"; +export const createFolderSorters = ( + getFilesCountInFolder: (folder: TFolder) => number +): Record number> => ({ + FolderNameAscending: (a, b) => a.name.localeCompare(b.name), + FolderNameDescending: (a, b) => b.name.localeCompare(a.name), + FilesCountAscending: (a, b) => + getFilesCountInFolder(a) - getFilesCountInFolder(b), + FilesCountDescending: (a, b) => + getFilesCountInFolder(b) - getFilesCountInFolder(a), + FolderManualOrder: () => 0, // special case +}); + export interface SortFolderSlice { folderSortRule: FolderSortRule; - isFoldersInAscendingOrder: () => boolean; - sortFolders: ( - folders: TFolder[], - rule: FolderSortRule, - includeSubfolderFiles: boolean - ) => TFolder[]; + folderSortRulesGroup: FolderSortRule[][]; + + sortFolders: (folders: TFolder[]) => TFolder[]; + changeFolderSortRule: (rule: FolderSortRule) => Promise; + changeFolderSortRuleAndUpdateOrder: (rule: FolderSortRule) => Promise; restoreFolderSortRule: () => Promise; } @@ -35,46 +46,42 @@ export const createSortFolderSlice = (set, get) => ({ folderSortRule: DEFAULT_FOLDER_SORT_RULE, - isFoldersInAscendingOrder: (): boolean => { - const { folderSortRule } = get(); - return folderSortRule.contains("Ascending"); + get folderSortRulesGroup(): FolderSortRule[][] { + return [ + ["FolderNameAscending", "FolderNameDescending"], + ["FilesCountAscending", "FilesCountDescending"], + ["FolderManualOrder"], + ]; }, - sortFolders: ( - folders: TFolder[], - rule: FolderSortRule, - includeSubfolder: boolean - ): TFolder[] => { + + sortFolders: (folders: TFolder[]): TFolder[] => { const { getFilesCountInFolder: getFilesCount, foldersManualSortOrder: order, + folderSortRule: rule, } = get(); - const parentPath = folders[0]?.parent?.path; - const folderPaths = parentPath ? order[parentPath] : []; - switch (rule) { - case "FolderNameAscending": - return folders.sort((a, b) => a.name.localeCompare(b.name)); - case "FolderNameDescending": - return folders.sort((a, b) => b.name.localeCompare(a.name)); - case "FilesCountAscending": - return folders.sort( - (a, b) => getFilesCount(a) - getFilesCount(b) - ); - case "FilesCountDescending": - return folders.sort( - (a, b) => getFilesCount(b) - getFilesCount(a) - ); - case "FolderManualOrder": - if (!parentPath || !folderPaths || !folderPaths.length) - return folders; - return folderPaths - .map((path) => folders.find((f) => f.path === path)) - .concat( - folders.filter((f) => !folderPaths.includes(f.path)) - ) - .filter(Boolean) as TFolder[]; - default: - return folders; + if (folders.length === 0) return folders; + + if (rule === "FolderManualOrder") { + const parentPath = folders[0]?.parent?.path; + if (!parentPath) return folders; + + const sortedFolderPaths = order[parentPath] ?? []; + if (!sortedFolderPaths.length) return folders; + + return [...folders].sort((a, b) => { + const aIndex = sortedFolderPaths.indexOf(a.path); + const bIndex = sortedFolderPaths.indexOf(b.path); + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + return aIndex - bIndex; + }); } + + const folderSorters = createFolderSorters(getFilesCount); + const sorter = folderSorters[rule]; + return [...folders].sort(sorter); }, changeFolderSortRule: async (rule: FolderSortRule) => { const { setValueAndSaveInPlugin } = get(); @@ -85,6 +92,22 @@ export const createSortFolderSlice = pluginValue: rule, }); }, + changeFolderSortRuleAndUpdateOrder: async (rule: FolderSortRule) => { + const { + folderSortRule, + changeFolderSortRule, + initFoldersManualSortOrder, + clearFoldersManualOrderAndSave, + } = get(); + if (rule !== folderSortRule) { + await changeFolderSortRule(rule as FolderSortRule); + if (rule === FOLDER_MANUAL_SORT_RULE) { + await initFoldersManualSortOrder(); + } else { + clearFoldersManualOrderAndSave(); + } + } + }, restoreFolderSortRule: async () => { const { restoreFoldersManualSortOrder, restoreDataFromPlugin } = get(); diff --git a/src/store/folder/structure.ts b/src/store/folder/structure.ts index 594a5f1..cf2f9ef 100644 --- a/src/store/folder/structure.ts +++ b/src/store/folder/structure.ts @@ -2,24 +2,35 @@ import { TFile, TFolder } from "obsidian"; import { StateCreator } from "zustand"; import FolderFileSplitterPlugin from "src/main"; +import { FOLDER_NOTE_LOCATION } from "src/settings"; import { isFile, isFolder } from "src/utils"; import { ExplorerStore } from ".."; +import { MARKDOWN_FILE_EXTENSION } from "../file/actions"; export interface FolderStructureSlice { folders: TFolder[]; - rootFolder: TFolder | null; + foldersWithRoot: TFolder[]; + rootFolder: TFolder; getNameOfFolder: (folder: TFolder) => string; + getFolderAncestors: (folder: TFolder) => TFolder[]; + isAnscestorOf: (ancestor: TFolder, folder: TFolder) => boolean; + getTopLevelFolders: () => TFolder[]; isTopLevelFolder: (folder: TFolder) => boolean; + isFolderPathValid: (path: string) => boolean; getSubFolders: (parentFolder: TFolder) => TFolder[]; - hasSubFolders: (folder: TFolder) => boolean; + hasSubFolder: (folder: TFolder) => boolean; getFilesInFolder: (folder: TFolder) => TFile[]; getFilesCountInFolder: (folder: TFolder) => number; + + getFolderNotePath: (folder: TFolder) => string; + + findFolderByPath: (path: string) => TFolder | null; } export const createFolderStructureSlice = @@ -28,23 +39,41 @@ export const createFolderStructureSlice = ): StateCreator => (set, get) => ({ folders: plugin.app.vault.getAllFolders() || [], - rootFolder: plugin.app.vault.getRoot() || null, + foldersWithRoot: plugin.app.vault.getAllFolders(true) || [], + rootFolder: plugin.app.vault.getRoot(), getNameOfFolder: (folder: TFolder) => { return folder.isRoot() ? plugin.app.vault.getName() : folder.name; }, + getFolderAncestors: (folder: TFolder): TFolder[] => { + const ancestors: TFolder[] = []; + let current = folder.parent; + + while (current && !current.isRoot()) { + ancestors.unshift(current); + current = current.parent; + } + + return ancestors; + }, + isAnscestorOf: (ancestor: TFolder, folder: TFolder): boolean => { + const ancestors = get().getFolderAncestors(folder); + return ancestors.some((f) => f.path === ancestor.path); + }, + + isFolderPathValid: (path: string) => { + return Boolean(get().findFolderByPath(path)); + }, isTopLevelFolder: (folder: TFolder): boolean => { return Boolean(folder.parent?.isRoot()); }, getTopLevelFolders: () => { - const { isTopLevelFolder } = get(); - return plugin.app.vault - .getAllFolders() - .filter((folder) => isTopLevelFolder(folder)); + const { isTopLevelFolder, folders } = get(); + return folders.filter((folder) => isTopLevelFolder(folder)); }, - hasSubFolders: (folder: TFolder): boolean => { + hasSubFolder: (folder: TFolder): boolean => { return folder.children.some((child) => isFolder(child)); }, getSubFolders: (parentFolder: TFolder): TFolder[] => { @@ -73,4 +102,24 @@ export const createFolderStructureSlice = const { getFilesInFolder } = get(); return getFilesInFolder(folder).length; }, + + getFolderNotePath: (folder: TFolder): string => { + const { folderNoteLocation, customFolderNotePath } = + plugin.settings; + const { INDEX_FILE, UNDERSCORE_FILE } = FOLDER_NOTE_LOCATION; + + if ([INDEX_FILE, UNDERSCORE_FILE].includes(folderNoteLocation)) { + return `${folder.path}/${folderNoteLocation}`; + } + + if (folderNoteLocation === FOLDER_NOTE_LOCATION.FOLDER_NAME_FILE) { + return `${folder.path}/${folder.name}${MARKDOWN_FILE_EXTENSION}`; + } + + return customFolderNotePath.replace("{folder}", folder.name || ""); + }, + + findFolderByPath: (path: string): TFolder | null => { + return plugin.app.vault.getFolderByPath(path); + }, }); diff --git a/src/store/folder/toggle.ts b/src/store/folder/toggle.ts index 65e85cc..ee39c99 100644 --- a/src/store/folder/toggle.ts +++ b/src/store/folder/toggle.ts @@ -10,11 +10,14 @@ import { ExplorerStore } from ".."; export interface ToggleFolderSlice { expandedFolderPaths: string[]; + isFolderExpanded: (folder: TFolder) => boolean; canFolderToggle: (folder: TFolder) => boolean; - changeExpandedFolderPaths: (folderNames: string[]) => Promise; - restoreExpandedFolderPaths: () => Promise; - expandFolder: (folder: TFolder) => Promise; - collapseFolder: (folder: TFolder) => Promise; + + changeExpandedFolderPaths: (folderNames: string[]) => void; + expandFolder: (folder: TFolder) => void; + collapseFolder: (folder: TFolder) => void; + + restoreExpandedFolderPaths: () => void; } export const createToggleFolderSlice = @@ -24,53 +27,60 @@ export const createToggleFolderSlice = (set, get) => ({ expandedFolderPaths: [], + isFolderExpanded: (folder: TFolder) => { + return get().expandedFolderPaths.includes(folder.path); + }, canFolderToggle: (folder: TFolder): boolean => { - const { hasSubFolders } = get(); + const { hasSubFolder } = get(); // root folder is always expanded - return !folder.isRoot() && hasSubFolders(folder); + return !folder.isRoot() && hasSubFolder(folder); }, - expandFolder: async (folder: TFolder) => { - const { - changeExpandedFolderPaths, - expandedFolderPaths, - canFolderToggle, - } = get(); - if (!canFolderToggle(folder)) return; - await changeExpandedFolderPaths( - uniq([...expandedFolderPaths, folder.path]) - ); + changeExpandedFolderPaths: (folderPaths: string[]) => { + const { setValueAndSaveInLocalStorage } = get(); + const paths = uniq(folderPaths); + setValueAndSaveInLocalStorage({ + key: "expandedFolderPaths", + value: paths, + localStorageKey: FFS_EXPANDED_FOLDER_PATHS_KEY, + localStorageValue: JSON.stringify(paths), + }); }, - collapseFolder: async (folder: TFolder) => { + expandFolder: (folder: TFolder) => { const { changeExpandedFolderPaths, expandedFolderPaths, canFolderToggle, } = get(); if (!canFolderToggle(folder)) return; - await changeExpandedFolderPaths( + changeExpandedFolderPaths([...expandedFolderPaths, folder.path]); + }, + collapseFolder: (folder: TFolder) => { + const { + changeExpandedFolderPaths, + expandedFolderPaths, + canFolderToggle, + } = get(); + if (!canFolderToggle(folder)) return; + changeExpandedFolderPaths( removeItemFromArray(expandedFolderPaths, folder.path) ); }, - changeExpandedFolderPaths: async (folderPaths: string[]) => { - const { setValueAndSaveInLocalStorage } = get(); - setValueAndSaveInLocalStorage({ - key: "expandedFolderPaths", - value: folderPaths, - localStorageKey: FFS_EXPANDED_FOLDER_PATHS_KEY, - localStorageValue: JSON.stringify(folderPaths), - }); - }, - restoreExpandedFolderPaths: async () => { - const { hasSubFolders, restoreDataFromLocalStorage } = get(); + + restoreExpandedFolderPaths: () => { + const { + hasSubFolder, + restoreDataFromLocalStorage, + findFolderByPath, + } = get(); restoreDataFromLocalStorage({ localStorageKey: FFS_EXPANDED_FOLDER_PATHS_KEY, key: "expandedFolderPaths", needParse: true, transform: (value) => { return (value as string[]).filter((path) => { - const folder = plugin.app.vault.getFolderByPath(path); - return folder && hasSubFolders(folder); + const folder = findFolderByPath(path); + return folder && hasSubFolder(folder); }); }, }); diff --git a/src/store/tag/focus.ts b/src/store/tag/focus.ts index 93fcdea..b02c3ee 100644 --- a/src/store/tag/focus.ts +++ b/src/store/tag/focus.ts @@ -30,15 +30,15 @@ export const createFocusedTagSlice = const { _setFocusedTag, focusedFile, - setFocusedFile, - setFocusedFolder, + setFocusedFileAndSave, + changeFocusedFolder, saveDataInLocalStorage, removeDataFromLocalStorage, } = get(); _setFocusedTag(tag); if (tag) { - setFocusedFolder(null); + changeFocusedFolder(null); saveDataInLocalStorage(FFS_FOCUSED_TAG_PATH_KEY, tag.fullPath); if (!focusedFile) return; @@ -46,7 +46,7 @@ export const createFocusedTagSlice = plugin.app.metadataCache.getFileCache(focusedFile)?.tags ?? []; if (tagsOfFocusedFile.every((t) => t.tag !== tag?.fullPath)) { - await setFocusedFile(null); + await setFocusedFileAndSave(null); } } else { removeDataFromLocalStorage(FFS_FOCUSED_TAG_PATH_KEY); diff --git a/src/store/tag/pin.ts b/src/store/tag/pin.ts index 2b7f989..a03d415 100644 --- a/src/store/tag/pin.ts +++ b/src/store/tag/pin.ts @@ -2,7 +2,6 @@ import { StateCreator } from "zustand"; import { FFS_PINNED_TAG_PATHS_KEY } from "src/assets/constants"; import FolderFileSplitterPlugin from "src/main"; -import { removeItemFromArray, uniq } from "src/utils"; import { ExplorerStore } from ".."; @@ -14,12 +13,12 @@ export interface PinnedTagSlice { getPinnedTags: () => TagNode[]; getDisplayedPinnedTags: () => TagNode[]; - isTagPinned: (tagPath: string) => boolean; + isTagPinned: (tag: TagNode) => boolean; setPinnedTagPathsAndSave: (tagPaths: string[]) => Promise; - pinTag: (tagPath: string) => Promise; - unpinTag: (tagPath: string) => Promise; + pinTag: (tag: TagNode) => Promise; + unpinTag: (tag: TagNode) => Promise; restorePinnedTags: () => Promise; } @@ -32,10 +31,10 @@ export const createPinnedTagSlice = pinnedTagPaths: [], getPinnedTags: () => { - const { tagTree, pinnedTagPaths } = get(); - return uniq(pinnedTagPaths) - .map((path) => tagTree.get(path)) - .filter(Boolean) as TagNode[]; + const { tagTree, pinnedTagPaths, getPinnedItems } = get(); + return getPinnedItems(pinnedTagPaths, (p) => + tagTree.get(p) + ); }, getDisplayedPinnedTags: () => { const { showTagView } = plugin.settings; @@ -44,44 +43,41 @@ export const createPinnedTagSlice = return getPinnedTags(); }, - isTagPinned: (tagPath: string) => { - const { pinnedTagPaths } = get(); - return pinnedTagPaths.includes(tagPath); + isTagPinned: (tag: TagNode) => { + const { pinnedTagPaths, isPinned } = get(); + return isPinned(pinnedTagPaths, tag.fullPath); }, setPinnedTagPathsAndSave: async (tagPaths: string[]) => { - const { setValueAndSaveInPlugin } = get(); - const paths = uniq(tagPaths); - await setValueAndSaveInPlugin({ - key: "pinnedTagPaths", - value: paths, - pluginKey: FFS_PINNED_TAG_PATHS_KEY, - pluginValue: JSON.stringify(paths), - }); + await get().setPinnedPathsAndSave( + "pinnedTagPaths", + FFS_PINNED_TAG_PATHS_KEY, + tagPaths + ); }, - pinTag: async (tagPath: string) => { - const { pinnedTagPaths, setPinnedTagPathsAndSave, isTagPinned } = - get(); - if (isTagPinned(tagPath)) return; - const tagPaths = [...pinnedTagPaths, tagPath]; - await setPinnedTagPathsAndSave(tagPaths); + pinTag: async (tag: TagNode) => { + const { pinnedTagPaths, setPinnedTagPathsAndSave, pinItem } = get(); + await pinItem( + tag.fullPath, + pinnedTagPaths, + setPinnedTagPathsAndSave + ); }, - unpinTag: async (tagPath: string) => { - const { pinnedTagPaths, setPinnedTagPathsAndSave, isTagPinned } = + unpinTag: async (tag: TagNode) => { + const { pinnedTagPaths, setPinnedTagPathsAndSave, unpinItem } = get(); - if (!isTagPinned(tagPath)) return; - await setPinnedTagPathsAndSave( - removeItemFromArray(pinnedTagPaths, tagPath) + await unpinItem( + tag.fullPath, + pinnedTagPaths, + setPinnedTagPathsAndSave ); }, restorePinnedTags: async () => { - const { restoreDataFromPlugin } = get(); - await restoreDataFromPlugin({ - pluginKey: FFS_PINNED_TAG_PATHS_KEY, - key: "pinnedTagPaths", - needParse: true, - }); + await get().restorePinnedPaths( + "pinnedTagPaths", + FFS_PINNED_TAG_PATHS_KEY + ); }, }); diff --git a/src/store/tag/sort.ts b/src/store/tag/sort.ts index 18f3c9e..6084b26 100644 --- a/src/store/tag/sort.ts +++ b/src/store/tag/sort.ts @@ -3,9 +3,22 @@ import { StateCreator } from "zustand"; import FolderFileSplitterPlugin from "src/main"; import { ExplorerStore } from ".."; +import { FolderSortRule } from "../folder/sort"; import { TagNode } from "."; +export const createTagSorters = ( + getFilesCountInTag: (tag: TagNode) => number +): Record number> => ({ + FolderNameAscending: (a, b) => a.name.localeCompare(b.name), + FolderNameDescending: (a, b) => b.name.localeCompare(a.name), + FilesCountAscending: (a, b) => + getFilesCountInTag(a) - getFilesCountInTag(b), + FilesCountDescending: (a, b) => + getFilesCountInTag(b) - getFilesCountInTag(a), + FolderManualOrder: () => 0, // special case +}); + export interface SortTagSlice { sortTags: (tags: TagNode[]) => TagNode[]; } @@ -17,21 +30,9 @@ export const createSortTagSlice = (set, get) => ({ sortTags: (tags: TagNode[]): TagNode[] => { const { getFilesCountInTag, folderSortRule: rule } = get(); - switch (rule) { - case "FolderNameAscending": - return tags.sort((a, b) => a.name.localeCompare(b.name)); - case "FolderNameDescending": - return tags.sort((a, b) => b.name.localeCompare(a.name)); - case "FilesCountAscending": - return tags.sort( - (a, b) => getFilesCountInTag(a) - getFilesCountInTag(b) - ); - case "FilesCountDescending": - return tags.sort( - (a, b) => getFilesCountInTag(b) - getFilesCountInTag(a) - ); - default: - return tags; - } + + const tagSorters = createTagSorters(getFilesCountInTag); + const sorter = tagSorters[rule]; + return [...tags].sort(sorter); }, }); diff --git a/src/store/tag/structure.ts b/src/store/tag/structure.ts index 44f7eb6..fcca203 100644 --- a/src/store/tag/structure.ts +++ b/src/store/tag/structure.ts @@ -7,21 +7,18 @@ import { ExplorerStore } from ".."; import { TagNode, TagTree } from "."; - export interface TagStructureSlice { tagTree: TagTree; generateTagTree: () => TagTree; - markdownFiles: TFile[]; - getTagsOfFile: (file: TFile) => string[]; getTopLevelTags: () => TagNode[]; isTopLevelTag: (tagNode: TagNode) => boolean; getFilesInTag: (tagNode: TagNode) => TFile[]; getFilesCountInTag: (tagNode: TagNode) => number; getTagsByParent: (parentTag: string) => TagNode[]; - hasTagChildren: (tagNode: TagNode) => boolean; + hasSubTag: (tagNode: TagNode) => boolean; } export const createTagStructureSlice = @@ -31,10 +28,6 @@ export const createTagStructureSlice = (set, get) => ({ tagTree: new Map(), - get markdownFiles() { - return plugin.app.vault.getMarkdownFiles(); - }, - getTagsOfFile: (file: TFile) => { const cache = plugin.app.metadataCache.getFileCache(file); if (!cache) return []; @@ -42,9 +35,11 @@ export const createTagStructureSlice = }, generateTagTree: () => { - const { _getOrCreateTagNode, markdownFiles, getTagsOfFile } = get(); + const { _getOrCreateTagNode, getMarkdownFiles, getTagsOfFile } = + get(); const tagTree: TagTree = new Map(); + const markdownFiles = getMarkdownFiles(); if (!markdownFiles || markdownFiles.length === 0) return tagTree; markdownFiles.forEach((file) => { @@ -136,7 +131,7 @@ export const createTagStructureSlice = return tags; }, - hasTagChildren: (tag: TagNode): boolean => { + hasSubTag: (tag: TagNode): boolean => { return tag.children && tag.children.size > 0; }, - }); + }); diff --git a/src/store/tag/toggle.ts b/src/store/tag/toggle.ts index 5722d0f..2e81c99 100644 --- a/src/store/tag/toggle.ts +++ b/src/store/tag/toggle.ts @@ -10,10 +10,15 @@ import { TagNode } from "."; export interface ToggleTagSlice { expandedTagPaths: string[]; - changeExpandedTagPaths: (paths: string[]) => Promise; - restoreExpandedTagPaths: () => Promise; + + isTagExpanded: (tag: TagNode) => boolean; + canTagToggle: (tag: TagNode) => boolean; + + changeExpandedTagPaths: (paths: string[]) => void; expandTag: (tag: TagNode) => Promise; collapseTag: (tag: TagNode) => Promise; + + restoreExpandedTagPaths: () => Promise; } export const createToggleTagSlice = @@ -23,36 +28,41 @@ export const createToggleTagSlice = (set, get) => ({ expandedTagPaths: [], - changeExpandedTagPaths: async (tagPaths: string[]) => { + isTagExpanded: (tag: TagNode) => { + return get().expandedTagPaths.includes(tag.fullPath); + }, + canTagToggle: (tag: TagNode) => { + return get().hasSubTag(tag); + }, + + changeExpandedTagPaths: (tagPaths: string[]) => { const { setValueAndSaveInLocalStorage } = get(); + const paths = uniq(tagPaths); setValueAndSaveInLocalStorage({ key: "expandedTagPaths", - value: tagPaths, + value: paths, localStorageKey: FFS_EXPANDED_TAG_PATHS_KEY, - localStorageValue: JSON.stringify(tagPaths), + localStorageValue: JSON.stringify(paths), }); }, expandTag: async (tag: TagNode) => { - const { changeExpandedTagPaths, expandedTagPaths, hasTagChildren } = + const { changeExpandedTagPaths, expandedTagPaths, canTagToggle } = get(); - if (!hasTagChildren(tag)) return; - await changeExpandedTagPaths( - uniq([...expandedTagPaths, tag.fullPath]) - ); + if (!canTagToggle(tag)) return; + await changeExpandedTagPaths([...expandedTagPaths, tag.fullPath]); }, collapseTag: async (tag: TagNode) => { - const { changeExpandedTagPaths, hasTagChildren, expandedTagPaths } = + const { changeExpandedTagPaths, canTagToggle, expandedTagPaths } = get(); - if (!hasTagChildren(tag)) return; + if (!canTagToggle(tag)) return; await changeExpandedTagPaths( removeItemFromArray(expandedTagPaths, tag.fullPath) ); }, restoreExpandedTagPaths: async () => { - const { hasTagChildren, tagTree, restoreDataFromLocalStorage } = - get(); + const { hasSubTag, tagTree, restoreDataFromLocalStorage } = get(); restoreDataFromLocalStorage({ localStorageKey: FFS_EXPANDED_TAG_PATHS_KEY, key: "expandedTagPaths", @@ -60,7 +70,7 @@ export const createToggleTagSlice = transform: (value) => { return (value as string[]).filter((path) => { const tag = tagTree.get(path); - return tag && hasTagChildren(tag); + return tag && hasSubTag(tag); }); }, }); diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 4c8355f..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { TAbstractFile, TFile, TFolder } from "obsidian"; - -type FolderChild = TFile | TFolder | TAbstractFile; -export const isFile = (item: FolderChild): item is TFile => { - return item instanceof TFile; -}; - -export const isFolder = (item: FolderChild): item is TFolder => { - return item instanceof TFolder; -}; - -export const isAbstractFileIncluded = ( - files: TAbstractFile[], - file: TAbstractFile -): boolean => files.some((f) => f.path === file.path); - -export const pluralize = (count: number, word: string): string => { - return `${count} ${word}${count > 1 ? "s" : ""}`; -}; - -export const uniq = (array: T[]): T[] => { - return Array.from(new Set(array)); -}; - -export const toValidNumber = (value: string | null): number | null => { - const num = Number(value); - return value !== null && !isNaN(num) ? num : null; -}; - -export const replaceItemInArray = ( - array: T[], - oldItem: T, - newItem: T -): T[] => { - const index = array.indexOf(oldItem); - if (index === -1) return array; - - const newArray = [...array]; - newArray.splice(index, 1, newItem); - return newArray; -}; - -export const moveItemInArray = ( - array: T[], - fromIndex: number, - toIndex: number -): T[] => { - if ( - fromIndex < 0 || - toIndex < 0 || - fromIndex >= array.length || - toIndex >= array.length || - fromIndex === toIndex - ) { - return array; - } - const newArray = [...array]; - const [item] = newArray.splice(fromIndex, 1); - newArray.splice(toIndex, 0, item); - return newArray; -}; - -export const removeItemFromArray = (array: T[], item: T): T[] => { - return array.filter((i) => i !== item); -}; - -type LogErrorOptions = { - name: string; - error: unknown; - data?: string; - params?: Record; -}; -export function logError({ - name, - error, - data, - params = {}, -}: LogErrorOptions): void { - const preview = - data && data.length > 200 ? data.slice(0, 200) + "..." : data; - const message = `[${name}] An error occurred.`; - console.error(message, { - error, - ...(preview ? { rawDataPreview: preview } : {}), - ...params, - }); -} diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000..3066b2d --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,73 @@ +export const uniq = (array: T[]): T[] => { + return Array.from(new Set(array)); +}; + +export const replaceItemInArray = ( + array: T[], + oldItem: T, + newItem: T +): T[] => { + const index = array.indexOf(oldItem); + if (index === -1) return array; + + const newArray = [...array]; + newArray.splice(index, 1, newItem); + return newArray; +}; + +export const moveItemInArray = ( + array: T[], + fromIndex: number, + toIndex: number +): T[] => { + if ( + fromIndex < 0 || + toIndex < 0 || + fromIndex >= array.length || + toIndex >= array.length || + fromIndex === toIndex + ) { + return array; + } + const newArray = [...array]; + const [item] = newArray.splice(fromIndex, 1); + newArray.splice(toIndex, 0, item); + return newArray; +}; + +export const removeItemFromArray = (array: T[], item: T): T[] => { + return array.filter((i) => i !== item); +}; + +export const appendMissingItems = ( + existing: T[], + expected: T[], + isMissing?: (item: T) => boolean +): T[] => { + const missingItems = expected.filter((item) => + isMissing ? isMissing(item) : !existing.includes(item) + ); + return [...existing, ...missingItems]; +}; + +export const areArraysEqual = ( + array1: T[], + array2: T[], + isEqual?: (a: T, b: T) => boolean +): boolean => { + if (array1.length !== array2.length) return false; + + const usedIndices = new Set(); + + for (const item1 of array1) { + const index = array2.findIndex((item2, i) => { + if (usedIndices.has(i)) return false; + return isEqual ? isEqual(item1, item2) : item1 === item2; + }); + + if (index === -1) return false; + usedIndices.add(index); + } + + return true; +}; diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..efb9895 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,51 @@ +import { Menu, TAbstractFile, TFile, TFolder } from "obsidian"; +import { MouseEvent } from "react"; + +import FolderFileSplitterPlugin from "src/main"; +import { FileSortRule } from "src/store/file/sort"; +import { FolderSortRule } from "src/store/folder/sort"; + +type FolderChild = TFile | TFolder | TAbstractFile; +export const isFile = (item: FolderChild): item is TFile => { + return item instanceof TFile; +}; + +export const isFolder = (item: FolderChild): item is TFolder => { + return item instanceof TFolder; +}; + +type MenuRule = { + title: string; + icon?: string; + checked?: boolean; + action: () => void; +}; +export const addMenuItem = (menu: Menu, rule: MenuRule): void => { + const { title, icon, checked, action } = rule; + menu.addItem((item) => { + item.setTitle(title); + if (icon) { + item.setIcon(icon); + } + if (checked) { + item.setChecked(checked); + } + item.onClick(action); + }); +}; + +export const triggerMenu = + (plugin: FolderFileSplitterPlugin, menu: Menu, menuName: string) => + (e: MouseEvent): void => { + plugin.app.workspace.trigger(menuName, menu); + menu.showAtPosition({ x: e.clientX, y: e.clientY }); + }; + +export type SortRule = FolderSortRule | FileSortRule; +export const isInAscendingOrderRule = (rule: SortRule): boolean => { + return rule.contains("Ascending"); +}; + +export const isManualSortOrderRule = (rule: SortRule): boolean => { + return rule.contains("ManualOrder"); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..4016310 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,34 @@ +export const pluralize = (count: number, word: string): string => { + return `${count} ${word}${count > 1 ? "s" : ""}`; +}; + +export const toValidNumber = (value: string | null): number | null => { + const num = Number(value); + return value !== null && !isNaN(num) ? num : null; +}; + +type LogErrorOptions = { + name: string; + error: unknown; + data?: string; + params?: Record; +}; +export function logError({ + name, + error, + data, + params = {}, +}: LogErrorOptions): void { + const preview = + data && data.length > 200 ? data.slice(0, 200) + "..." : data; + const message = `[${name}] An error occurred.`; + console.error(message, { + error, + ...(preview ? { rawDataPreview: preview } : {}), + ...params, + }); +} + +export * from "./array"; +export * from "./file"; +export * from "./string"; diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..cb4ab6d --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,40 @@ +export const getAvailableName = ( + existingNames: string[], + baseName: string +): string => { + const usedNumbers = new Set(); + + existingNames.forEach((name) => { + if (name === baseName) { + usedNumbers.add(0); + } else if (name.startsWith(`${baseName} `)) { + const suffix = name.slice(baseName.length + 1).trim(); + const number = parseInt(suffix, 10); + if (!isNaN(number)) { + usedNumbers.add(number); + } + } + }); + + for (let i = 0; i <= usedNumbers.size + 1; i++) { + if (!usedNumbers.has(i)) { + return i === 0 ? baseName : `${baseName} ${i}`; + } + } + + return `${baseName} ${usedNumbers.size + 1}`; +}; + +export const getDefaultUntitledName = (existingNames: string[]): string => { + const baseName = "Untitled"; + return getAvailableName(existingNames, baseName); +}; + +export const getCopyName = ( + existingNames: string[], + originalName: string +): string => { + const baseCopyName = `${originalName} copy`; + + return getAvailableName(existingNames, baseCopyName); +};