refactor: optimize function names for better readability and split long functions for better management

This commit is contained in:
Xu Quan 2025-06-30 17:45:39 +08:00
parent 433893585e
commit 6ae2bfabac
56 changed files with 1762 additions and 1353 deletions

View file

@ -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>
);
};

View file

@ -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);
}
};

View file

@ -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 = () => {

View file

@ -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}

View file

@ -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]}
/>
);
};

View file

@ -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} />

View file

@ -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);
}
});
});

View file

@ -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)}
/>
);
};

View file

@ -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()}

View file

@ -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();
};

View file

@ -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]}
/>
);
};

View file

@ -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 = () => {

View file

@ -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))
)}

View file

@ -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) => {

View file

@ -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;

View file

@ -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()}

View file

@ -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;

View file

@ -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} />

View file

@ -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);
}
});
});

View file

@ -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)}
/>
);
};

View file

@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";

View file

@ -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,
}
);

View file

@ -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);
}
};
};

View file

@ -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

View file

@ -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();

View file

@ -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: "对文件夹和标签排序",
},
};

View file

@ -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
View 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";

View 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,
},
});
}
},
});

View 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
View 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
View 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,
},
});
}
},
});

View 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);
},
});

View file

@ -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);
}
},
});

View file

@ -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);
},
});

View file

@ -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);
},
});

View file

@ -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
);
},
});

View file

@ -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();

View file

@ -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));
},
});

View file

@ -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) => {

View file

@ -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),
});
},
});

View file

@ -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);
},
});

View file

@ -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
);
},
});

View file

@ -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();

View file

@ -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);
},
});

View file

@ -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);
});
},
});

View file

@ -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);

View file

@ -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
);
},
});

View file

@ -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);
},
});

View file

@ -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;
},
});
});

View file

@ -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);
});
},
});

View file

@ -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
View 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
View 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
View 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
View 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);
};