mirror of
https://github.com/xuquan-nikkkki/FolderFile-Splitter-Plugin.git
synced 2026-07-22 05:37:28 +00:00
refactor: optimize function names for better readability and split long functions for better management
This commit is contained in:
parent
433893585e
commit
6ae2bfabac
56 changed files with 1762 additions and 1353 deletions
|
|
@ -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 = (
|
||||
<ChevronRight className="ffs__expand-icon svg-icon right-triangle" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("tree-item-icon collapse-icon", {
|
||||
"is-collapsed": !isExpanded,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
{hideIcon ? null : (
|
||||
<ChevronRight className="ffs__expand-icon svg-icon right-triangle" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SortAction
|
||||
plugin={plugin}
|
||||
ruleGroups={getRuleGroups()}
|
||||
menuName="sort-files-menu"
|
||||
changeSortRule={onChangeSortRule}
|
||||
isInAscendingOrder={isFilesInAscendingOrder}
|
||||
currentSortRule={fileSortRule}
|
||||
isManualOrder={fileSortRule === FILE_MANUAL_SORT_RULE}
|
||||
data-tooltip-position="bottom"
|
||||
aria-label={getAriaLabel()}
|
||||
aria-label={SORT_TIPS_COPY.sortFiles[language]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="ffs__tree ffs__file-tree nav-files-container">
|
||||
<PinnedFiles renderFile={renderFile} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ExpandIcon
|
||||
isExpanded={isFolderExpanded}
|
||||
hideIcon={!hasSubFolders(folder)}
|
||||
hideIcon={!hasSubFolder(folder)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SortAction
|
||||
plugin={plugin}
|
||||
ruleGroups={getRuleGroups()}
|
||||
menuName="sort-folders-menu"
|
||||
changeSortRule={onChangeSortRule}
|
||||
isInAscendingOrder={isFoldersInAscendingOrder}
|
||||
currentSortRule={folderSortRule}
|
||||
isManualOrder={folderSortRule === FOLDER_MANUAL_SORT_RULE}
|
||||
data-tooltip-position="bottom"
|
||||
aria-label={getAriaLabel()}
|
||||
aria-label={SORT_TIPS_COPY.sortFoldersAndTags[language]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ const ToggleFolders = () => {
|
|||
const [isExpanded, setIsExpanded] = useState<boolean>(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 = () => {
|
||||
|
|
|
|||
|
|
@ -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)))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -195,7 +195,7 @@ const FolderAndTagTree = ({ onOpenFilesPane = () => {} }: Props) => {
|
|||
>
|
||||
{renderTag(tag)}
|
||||
{isExpanded &&
|
||||
hasTagChildren(tag) &&
|
||||
hasSubTag(tag) &&
|
||||
renderSubTags(
|
||||
renderTags(getTagsByParent(tag.fullPath))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<TFile | null>(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) => {
|
||||
|
|
|
|||
|
|
@ -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<StoreApi<ExplorerStore>>
|
||||
useExplorerStore: UseBoundStore<StoreApi<ExplorerStore>>,
|
||||
folder: TFolder | null = plugin.app.vault.getRoot()
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Fragment key={crumb + index}>
|
||||
{index > 0 && renderSlashSign()}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export class ManualSortFoldersModal extends Modal {
|
|||
|
||||
constructor(
|
||||
plugin: FolderFileSplitterPlugin,
|
||||
useExplorerStore: UseBoundStore<StoreApi<ExplorerStore>>,
|
||||
folder: TFolder,
|
||||
useExplorerStore: UseBoundStore<StoreApi<ExplorerStore>>
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>) => {
|
||||
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 <ArrowUpDownIcon className={actionButtonClassName} />;
|
||||
}
|
||||
return isInAscendingOrder() ? (
|
||||
return isInAscendingOrderRule(currentSortRule) ? (
|
||||
<AscendingSortIcon className={actionButtonClassName} />
|
||||
) : (
|
||||
<DescendingSortIcon className={actionButtonClassName} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ExpandIcon
|
||||
isExpanded={isTagExpanded}
|
||||
hideIcon={!hasTagChildren(tag)}
|
||||
isExpanded={isTagExpanded(tag)}
|
||||
hideIcon={!hasSubTag(tag)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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: "对文件夹和标签排序",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<typeof VIEW_MODE>;
|
||||
export const DEFAULT_VIEW_MODE: ViewMode = VIEW_MODE.FOLDER;
|
||||
|
||||
type FolderPath = string;
|
||||
type ChildrenPaths = string[];
|
||||
export type ManualSortOrder = Record<FolderPath, ChildrenPaths>;
|
||||
|
||||
type SetValueAndSaveInLocalStorageParams<T> = {
|
||||
key: string;
|
||||
value: T;
|
||||
localStorageKey: string;
|
||||
localStorageValue: string;
|
||||
};
|
||||
type SetValueAndSaveInPluginParams<T> = {
|
||||
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: <T>(key: string) => Promise<T | undefined>;
|
||||
saveDataInPlugin: (oata: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
setValueAndSaveInLocalStorage: <T>({
|
||||
key,
|
||||
value,
|
||||
localStorageKey,
|
||||
localStorageValue,
|
||||
}: SetValueAndSaveInLocalStorageParams<T>) => void;
|
||||
setValueAndSaveInPlugin: <T>({
|
||||
key,
|
||||
value,
|
||||
pluginKey,
|
||||
pluginValue,
|
||||
}: SetValueAndSaveInPluginParams<T>) => Promise<void>;
|
||||
restoreDataFromLocalStorage: <T>({
|
||||
localStorageKey,
|
||||
key,
|
||||
needParse,
|
||||
transform,
|
||||
}: RestoreDataFromLocalStorageParams) => T;
|
||||
restoreDataFromPlugin: ({
|
||||
pluginKey,
|
||||
key,
|
||||
needParse,
|
||||
}: RestoreDataFromPluginParams) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export const createCommonExplorerStore =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], CommonExplorerStore> =>
|
||||
(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<string, unknown>
|
||||
): Promise<void> => {
|
||||
const previousData = await plugin.loadData();
|
||||
await plugin.saveData({
|
||||
...previousData,
|
||||
...data,
|
||||
});
|
||||
},
|
||||
getDataFromPlugin: async <T>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
const data = await plugin.loadData();
|
||||
return data[key];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
setValueAndSaveInLocalStorage: <T>({
|
||||
key,
|
||||
value,
|
||||
localStorageKey,
|
||||
localStorageValue,
|
||||
}: SetValueAndSaveInLocalStorageParams<T>) => {
|
||||
const { saveDataInLocalStorage, removeDataFromLocalStorage } =
|
||||
get();
|
||||
|
||||
set({ [key]: value } as Partial<ExplorerStore>);
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
removeDataFromLocalStorage(localStorageKey);
|
||||
} else {
|
||||
saveDataInLocalStorage(localStorageKey, localStorageValue);
|
||||
}
|
||||
},
|
||||
setValueAndSaveInPlugin: async <T>({
|
||||
key,
|
||||
value,
|
||||
pluginKey,
|
||||
pluginValue,
|
||||
}: SetValueAndSaveInPluginParams<T>): Promise<void> => {
|
||||
const { saveDataInPlugin } = get();
|
||||
set({ [key]: value } as Partial<ExplorerStore>);
|
||||
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<ExplorerStore>);
|
||||
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<string>(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<ExplorerStore>);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
logError({
|
||||
name: "restoreDataFromPlugin",
|
||||
error,
|
||||
data: raw,
|
||||
params: {
|
||||
key,
|
||||
pluginKey,
|
||||
needParse,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
39
src/store/common/index.ts
Normal file
39
src/store/common/index.ts
Normal file
|
|
@ -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<FolderPath, ChildrenPaths>;
|
||||
export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {};
|
||||
|
||||
export type CommonExplorerStore = ViewModeSlice &
|
||||
LocalStorageSlice &
|
||||
PluginSlice &
|
||||
ManualSortSlice &
|
||||
PinSlice;
|
||||
|
||||
export const createCommonExplorerStore =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], CommonExplorerStore> =>
|
||||
(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";
|
||||
116
src/store/common/localStorage.ts
Normal file
116
src/store/common/localStorage.ts
Normal file
|
|
@ -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<FolderPath, ChildrenPaths>;
|
||||
export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {};
|
||||
|
||||
type SetValueAndSaveInLocalStorageParams<T> = {
|
||||
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: <T>({
|
||||
key,
|
||||
value,
|
||||
localStorageKey,
|
||||
localStorageValue,
|
||||
}: SetValueAndSaveInLocalStorageParams<T>) => void;
|
||||
restoreDataFromLocalStorage: <T>({
|
||||
localStorageKey,
|
||||
key,
|
||||
needParse,
|
||||
transform,
|
||||
}: RestoreDataFromLocalStorageParams) => T;
|
||||
};
|
||||
|
||||
export const createLocalStorageSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], LocalStorageSlice> =>
|
||||
(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: <T>({
|
||||
key,
|
||||
value,
|
||||
localStorageKey,
|
||||
localStorageValue,
|
||||
}: SetValueAndSaveInLocalStorageParams<T>) => {
|
||||
const { saveDataInLocalStorage, removeDataFromLocalStorage } =
|
||||
get();
|
||||
|
||||
set({ [key]: value } as Partial<ExplorerStore>);
|
||||
|
||||
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<ExplorerStore>);
|
||||
return finalData;
|
||||
} catch (error) {
|
||||
logError({
|
||||
name: "restoreDataFromLocalStorage",
|
||||
error,
|
||||
data: raw,
|
||||
params: {
|
||||
key,
|
||||
localStorageKey,
|
||||
needParse,
|
||||
hasTransform: !!transform,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
177
src/store/common/manualSort.ts
Normal file
177
src/store/common/manualSort.ts
Normal file
|
|
@ -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<FolderPath, ChildrenPaths>;
|
||||
export const DEFAULT_MANUAL_SORT_ORDER: ManualSortOrder = {};
|
||||
|
||||
type UpdateOrderFn = (updatedOrder: ManualSortOrder) => Promise<void>;
|
||||
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<ManualSortOrder>;
|
||||
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<void>;
|
||||
updatePathInManualOrder: (
|
||||
order: ManualSortOrder,
|
||||
updateOrder: UpdateOrderFn,
|
||||
params: UpdatePathInManualOrderPathParams
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export const createManualSortSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], ManualSortSlice> =>
|
||||
(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<ManualSortOrder>(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<void>
|
||||
) => {
|
||||
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<void>,
|
||||
{ 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);
|
||||
},
|
||||
});
|
||||
119
src/store/common/pin.ts
Normal file
119
src/store/common/pin.ts
Normal file
|
|
@ -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: <T extends PinnedItem>(
|
||||
paths: string[],
|
||||
getter: (path: string) => T | undefined
|
||||
) => T[];
|
||||
|
||||
isPinned: (pinnedPaths: string[], path: string) => boolean;
|
||||
setPinnedPathsAndSave: (
|
||||
key: string,
|
||||
pluginKey: string,
|
||||
paths: string[]
|
||||
) => Promise<void>;
|
||||
pinItem: (
|
||||
path: string,
|
||||
paths: string[],
|
||||
save: (paths: string[]) => Promise<void>
|
||||
) => Promise<void>;
|
||||
unpinItem: (
|
||||
path: string,
|
||||
paths: string[],
|
||||
save: (paths: string[]) => Promise<void>
|
||||
) => Promise<void>;
|
||||
updatePinnedPath: (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
pinnedPaths: string[],
|
||||
save: (paths: string[]) => Promise<void>
|
||||
) => Promise<void>;
|
||||
restorePinnedPaths: (key: string, pluginKey: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const createPinSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], PinSlice> =>
|
||||
(set, get) => ({
|
||||
getPinnedItems: <T extends PinnedItem>(
|
||||
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<void>
|
||||
) => {
|
||||
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<void>
|
||||
) => {
|
||||
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<void>
|
||||
) => {
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
110
src/store/common/plugin.ts
Normal file
110
src/store/common/plugin.ts
Normal file
|
|
@ -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<T> = {
|
||||
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: <T>(key: string) => Promise<T | undefined>;
|
||||
saveDataInPlugin: (oata: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
setValueAndSaveInPlugin: <T>({
|
||||
key,
|
||||
value,
|
||||
pluginKey,
|
||||
pluginValue,
|
||||
}: SetValueAndSaveInPluginParams<T>) => Promise<void>;
|
||||
restoreDataFromPlugin: ({
|
||||
pluginKey,
|
||||
key,
|
||||
needParse,
|
||||
}: RestoreDataFromPluginParams) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export const createPluginSlice =
|
||||
(
|
||||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], PluginSlice> =>
|
||||
(set, get, store) => ({
|
||||
saveDataInPlugin: async (
|
||||
data: Record<string, unknown>
|
||||
): Promise<void> => {
|
||||
const previousData = await plugin.loadData();
|
||||
await plugin.saveData({
|
||||
...previousData,
|
||||
...data,
|
||||
});
|
||||
},
|
||||
getDataFromPlugin: async <T>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
const data = await plugin.loadData();
|
||||
return data[key];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
setValueAndSaveInPlugin: async <T>({
|
||||
key,
|
||||
value,
|
||||
pluginKey,
|
||||
pluginValue,
|
||||
}: SetValueAndSaveInPluginParams<T>): Promise<void> => {
|
||||
const { saveDataInPlugin } = get();
|
||||
set({ [key]: value } as Partial<ExplorerStore>);
|
||||
await saveDataInPlugin({ [pluginKey]: pluginValue });
|
||||
},
|
||||
restoreDataFromPlugin: async ({
|
||||
pluginKey,
|
||||
key,
|
||||
needParse = false,
|
||||
transform,
|
||||
validate,
|
||||
}: RestoreDataFromPluginParams) => {
|
||||
const { getDataFromPlugin } = get();
|
||||
const raw = await getDataFromPlugin<string>(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<ExplorerStore>);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
logError({
|
||||
name: "restoreDataFromPlugin",
|
||||
error,
|
||||
data: raw,
|
||||
params: {
|
||||
key,
|
||||
pluginKey,
|
||||
needParse,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
63
src/store/common/viewMode.ts
Normal file
63
src/store/common/viewMode.ts
Normal file
|
|
@ -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<typeof VIEW_MODE>;
|
||||
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<ExplorerStore, [], [], ViewModeSlice> =>
|
||||
(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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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<string>;
|
||||
|
||||
createFileAndOpen: (path: string, focus?: boolean) => Promise<TFile>;
|
||||
createFile: (folder: TFolder) => Promise<TFile | undefined>;
|
||||
duplicateFile: (file: TFile) => Promise<TFile>;
|
||||
trashFile: (file: TFile) => Promise<void>;
|
||||
|
||||
moveFile: (file: TFile, newPath: string) => Promise<void>;
|
||||
renameFile: (file: TFile, newName: string) => Promise<void>;
|
||||
openFile: (file: TFile, focus?: boolean) => void;
|
||||
selectFile: (file: TFile, focus?: boolean) => Promise<void>;
|
||||
readFile: (file: TFile) => Promise<string>;
|
||||
|
||||
trashFile: (file: TFile) => Promise<void>;
|
||||
}
|
||||
|
||||
export const createFileActionsSlice =
|
||||
|
|
@ -22,79 +32,62 @@ export const createFileActionsSlice =
|
|||
plugin: FolderFileSplitterPlugin
|
||||
): StateCreator<ExplorerStore, [], [], FileActionsSlice> =>
|
||||
(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<void> => {
|
||||
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<string> => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
setFocusedFileAndSave: (file: TFile | null) => void;
|
||||
restoreLastFocusedFile: () => Promise<void>;
|
||||
clearFocusedFile: () => Promise<void>;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
getInitialFilesOrder: () => ManualSortOrder;
|
||||
getRestoredFilesOrder: () => Promise<ManualSortOrder>;
|
||||
|
||||
setFilesManualOrderAndSave: (
|
||||
updatedOrder: ManualSortOrder
|
||||
) => Promise<void>;
|
||||
|
||||
initFilesManualSortOrder: () => Promise<void>;
|
||||
restoreFilesManualSortOrder: () => Promise<void>;
|
||||
updateFileManualOrder: (
|
||||
updateFilePathInManualOrder: (
|
||||
parentPath: string,
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
) => Promise<void>;
|
||||
changeFilesManualOrder: (
|
||||
file: TFile,
|
||||
atIndex: number
|
||||
) => ManualSortOrder | undefined;
|
||||
changeFilesManualOrderAndSave: (
|
||||
file: TFile,
|
||||
atIndex: number
|
||||
) => Promise<void>;
|
||||
_updateAndSaveFilesOrder: (updatedOrder: ManualSortOrder) => Promise<void>;
|
||||
moveFileInManualOrder: (file: TFile, atIndex: number) => Promise<void>;
|
||||
clearFileManualOrderAndSave: () => Promise<void>;
|
||||
}
|
||||
|
||||
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<ManualSortOrder>(
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TFile>(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
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<FileSortRule, (a: TFile, b: TFile) => 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<void>;
|
||||
changeFileSortRule: (rule: FileSortRule) => Promise<void>;
|
||||
changeFileSortRuleAndUpdateOrder: (rule: FileSortRule) => Promise<void>;
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<ExplorerStore, [], [], FileStructureSlice> =>
|
||||
(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));
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TFolder>;
|
||||
|
||||
getNewFolderDefaultName: (parentFolder: TFolder) => string;
|
||||
|
||||
createNewFolder: (parentFolder: TFolder) => Promise<TFolder | undefined>;
|
||||
trashFolder: (folder: TFolder) => Promise<void>;
|
||||
|
||||
moveFolder: (folder: TFolder, newPath: string) => Promise<void>;
|
||||
renameFolder: (folder: TFolder, newName: string) => Promise<void>;
|
||||
|
||||
trashFolder: (folder: TFolder) => Promise<void>;
|
||||
}
|
||||
|
||||
export const createFolderActionsSlice =
|
||||
|
|
@ -24,57 +28,36 @@ export const createFolderActionsSlice =
|
|||
latestCreatedFolder: null,
|
||||
latestFolderCreatedTime: null,
|
||||
|
||||
_createFolder: async (path: string): Promise<TFolder> => {
|
||||
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<TFolder | undefined> => {
|
||||
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<TFolder | undefined> => {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
restoreLastFocusedFolder: () => Promise<void>;
|
||||
|
||||
_setFocusedFolder: (folder: TFolder | null) => void;
|
||||
setFocusedFolder: (folder: TFolder | null) => Promise<void>;
|
||||
restoreLastFocusedFolder: () => Promise<void>;
|
||||
openFolderNote: (folder: TFolder) => Promise<void>;
|
||||
handleMissingFolderNote: (folderNotePath: string) => Promise<void>;
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
getInitialFoldersOrder: () => ManualSortOrder;
|
||||
getRestoredFoldersOrder: () => Promise<ManualSortOrder>;
|
||||
|
||||
initFoldersManualSortOrder: () => Promise<void>;
|
||||
restoreFoldersManualSortOrder: () => Promise<void>;
|
||||
changeFoldersManualOrder: (
|
||||
folder: TFolder,
|
||||
atIndex: number
|
||||
) => ManualSortOrder | undefined;
|
||||
changeFoldersManualOrderAndSave: (
|
||||
moveFolderInManualOrder: (
|
||||
folder: TFolder,
|
||||
atIndex: number
|
||||
) => Promise<void>;
|
||||
_updateAndSaveFoldersOrder: (
|
||||
setFoldersManualOrderAndSave: (
|
||||
updatedOrder: ManualSortOrder
|
||||
) => Promise<void>;
|
||||
_updateFolderManualOrder: (
|
||||
updateFolderPathInManualOrder: (
|
||||
parentPath: string,
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
) => Promise<void>;
|
||||
clearFoldersManualOrderAndSave: () => Promise<void>;
|
||||
}
|
||||
|
||||
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<ManualSortOrder>(
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TFolder>(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
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<FolderSortRule, (a: TFolder, b: TFolder) => 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<void>;
|
||||
changeFolderSortRuleAndUpdateOrder: (rule: FolderSortRule) => Promise<void>;
|
||||
restoreFolderSortRule: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<ExplorerStore, [], [], FolderStructureSlice> =>
|
||||
(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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ import { ExplorerStore } from "..";
|
|||
export interface ToggleFolderSlice {
|
||||
expandedFolderPaths: string[];
|
||||
|
||||
isFolderExpanded: (folder: TFolder) => boolean;
|
||||
canFolderToggle: (folder: TFolder) => boolean;
|
||||
changeExpandedFolderPaths: (folderNames: string[]) => Promise<void>;
|
||||
restoreExpandedFolderPaths: () => Promise<void>;
|
||||
expandFolder: (folder: TFolder) => Promise<void>;
|
||||
collapseFolder: (folder: TFolder) => Promise<void>;
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
|
||||
pinTag: (tagPath: string) => Promise<void>;
|
||||
unpinTag: (tagPath: string) => Promise<void>;
|
||||
pinTag: (tag: TagNode) => Promise<void>;
|
||||
unpinTag: (tag: TagNode) => Promise<void>;
|
||||
|
||||
restorePinnedTags: () => Promise<void>;
|
||||
}
|
||||
|
|
@ -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<TagNode>(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
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<FolderSortRule, (a: TagNode, b: TagNode) => 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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ import { TagNode } from ".";
|
|||
|
||||
export interface ToggleTagSlice {
|
||||
expandedTagPaths: string[];
|
||||
changeExpandedTagPaths: (paths: string[]) => Promise<void>;
|
||||
restoreExpandedTagPaths: () => Promise<void>;
|
||||
|
||||
isTagExpanded: (tag: TagNode) => boolean;
|
||||
canTagToggle: (tag: TagNode) => boolean;
|
||||
|
||||
changeExpandedTagPaths: (paths: string[]) => void;
|
||||
expandTag: (tag: TagNode) => Promise<void>;
|
||||
collapseTag: (tag: TagNode) => Promise<void>;
|
||||
|
||||
restoreExpandedTagPaths: () => Promise<void>;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
87
src/utils.ts
87
src/utils.ts
|
|
@ -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 = <T>(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 = <T>(
|
||||
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 = <T>(
|
||||
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 = <T>(array: T[], item: T): T[] => {
|
||||
return array.filter((i) => i !== item);
|
||||
};
|
||||
|
||||
type LogErrorOptions = {
|
||||
name: string;
|
||||
error: unknown;
|
||||
data?: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
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,
|
||||
});
|
||||
}
|
||||
73
src/utils/array.ts
Normal file
73
src/utils/array.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
export const uniq = <T>(array: T[]): T[] => {
|
||||
return Array.from(new Set(array));
|
||||
};
|
||||
|
||||
export const replaceItemInArray = <T>(
|
||||
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 = <T>(
|
||||
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 = <T>(array: T[], item: T): T[] => {
|
||||
return array.filter((i) => i !== item);
|
||||
};
|
||||
|
||||
export const appendMissingItems = <T>(
|
||||
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 = <T>(
|
||||
array1: T[],
|
||||
array2: T[],
|
||||
isEqual?: (a: T, b: T) => boolean
|
||||
): boolean => {
|
||||
if (array1.length !== array2.length) return false;
|
||||
|
||||
const usedIndices = new Set<number>();
|
||||
|
||||
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;
|
||||
};
|
||||
51
src/utils/file.ts
Normal file
51
src/utils/file.ts
Normal file
|
|
@ -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<HTMLDivElement>): 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");
|
||||
};
|
||||
34
src/utils/index.ts
Normal file
34
src/utils/index.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
};
|
||||
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";
|
||||
40
src/utils/string.ts
Normal file
40
src/utils/string.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export const getAvailableName = (
|
||||
existingNames: string[],
|
||||
baseName: string
|
||||
): string => {
|
||||
const usedNumbers = new Set<number>();
|
||||
|
||||
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);
|
||||
};
|
||||
Loading…
Reference in a new issue