feat: support pin folders

This commit is contained in:
Xu Quan 2025-03-05 17:26:28 +08:00
parent a689e0ff51
commit e59cd6489d
7 changed files with 152 additions and 12 deletions

View file

@ -1,14 +1,11 @@
import { TAbstractFile } from "obsidian";
export const FFS_FOCUSED_FOLDER_PATH_KEY =
"FocusedFolderPath";
export const FFS_EXPANDED_FOLDER_PATHS_KEY =
"ExpandedFolderPaths";
export const FFS_FOCUSED_FILE_PATH_KEY =
"FocusedFilePath";
export const FFS_FOLDER_SORT_RULE_KEY =
"FolderSortRule";
export const FFS_FOCUSED_FOLDER_PATH_KEY = "FocusedFolderPath";
export const FFS_EXPANDED_FOLDER_PATHS_KEY = "ExpandedFolderPaths";
export const FFS_FOCUSED_FILE_PATH_KEY = "FocusedFilePath";
export const FFS_FOLDER_SORT_RULE_KEY = "FolderSortRule";
export const FFS_FILE_SORT_RULE_KEY = "FileSortRule";
export const FFS_PINNED_FOLDER_PATHS_KEY = "PinnedFolderPaths";
export const FFS_FOLDER_PANE_WIDTH_KEY =
"FolderFileSplitterPlugin-FolderPaneWidth";

View file

@ -0,0 +1,16 @@
const PinIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="ffs-icon ffs-pin-icon"
>
<path d="M12 17v5" />
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />
</svg>
);
export default PinIcon

View file

@ -57,6 +57,9 @@ const Folder = ({
createFile,
folders,
focusedFile,
pinFolder,
unpinFolder,
isFolderPinned,
} = useFileTreeStore(
useShallow((store: FileTreeStore) => ({
hasFolderChildren: store.hasFolderChildren,
@ -68,6 +71,9 @@ const Folder = ({
createFile: store.createFile,
folders: store.folders,
focusedFile: store.focusedFile,
pinFolder: store.pinFolder,
unpinFolder: store.unpinFolder,
isFolderPinned: store.isFolderPinned,
}))
);
@ -157,6 +163,19 @@ const Folder = ({
e.stopPropagation();
const menu = new Menu();
menu.addItem((item) => {
const isPinned = isFolderPinned(folder);
const title = isPinned ? "Unpin folder" : "Pin folder";
item.setTitle(title);
item.onClick(() => {
if (isPinned) {
unpinFolder(folder);
} else {
pinFolder(folder);
}
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle("New note");
item.onClick(() => {

View file

@ -8,6 +8,7 @@ import { FileTreeStore } from "src/store";
import Folder from "./Folder";
import { useShowHierarchyLines } from "src/hooks/useSettingsHandler";
import { useChangeFolder } from "src/hooks/useVaultChangeHandler";
import PinIcon from "src/assets/icons/PinIcon";
type Props = {
useFileTreeStore: UseBoundStore<StoreApi<FileTreeStore>>;
@ -31,6 +32,7 @@ const Folders = ({ useFileTreeStore, plugin }: Props) => {
sortFolders: store.sortFolders,
expandedFolderPaths: store.expandedFolderPaths,
focusedFolder: store.focusedFolder,
pinnedFolders: store.pinnedFolderPaths,
}))
);
@ -66,6 +68,7 @@ const Folders = ({ useFileTreeStore, plugin }: Props) => {
const renderFolder = (folder: TFolder, isRoot?: boolean) => (
<Folder
key={folder.path}
folder={folder}
useFileTreeStore={useFileTreeStore}
plugin={plugin}
@ -110,8 +113,28 @@ const Folders = ({ useFileTreeStore, plugin }: Props) => {
);
};
const renderPinnedFolders = () => {
const pinnedFolderPaths = useFileTreeStore.getState().pinnedFolderPaths;
if (!pinnedFolderPaths.length) return null;
return (
<div className="ffs-pinned-folders-section">
<span className="ffs-pinned-title">
<PinIcon />
Pin
</span>
<div className="ffs-pinned-folders">
{pinnedFolderPaths.map((path) => {
const folder = plugin.app.vault.getFolderByPath(path);
return folder ? renderFolder(folder) : null;
})}
</div>
</div>
);
};
return (
<div ref={foldersRef}>
{renderPinnedFolders()}
{renderRootFolder()}
{renderFolders(topFolders)}
</div>

View file

@ -10,8 +10,8 @@ const Loading = ({ width = 30, height = 30 }: Props) => {
<div className="loading-icon">
<LoadingIcon />
</div>
<p>Loading...</p>
<style jsx>
<p>Loading...</p>
<style>
{`
.loading {
height: 100%;

View file

@ -2,13 +2,20 @@ import { create } from "zustand";
import { TFile, TFolder } from "obsidian";
import FolderFileSplitterPlugin from "./main";
import { isFile, isFolder } from "./utils";
import {
addItem,
isAbstractFileIncluded,
isFile,
isFolder,
removeItem,
} from "./utils";
import {
FFS_EXPANDED_FOLDER_PATHS_KEY,
FFS_FILE_SORT_RULE_KEY,
FFS_FOCUSED_FILE_PATH_KEY,
FFS_FOCUSED_FOLDER_PATH_KEY,
FFS_FOLDER_SORT_RULE_KEY,
FFS_PINNED_FOLDER_PATHS_KEY,
} from "./assets/constants";
export type FolderSortRule =
@ -30,6 +37,7 @@ export type FileTreeStore = {
folders: TFolder[];
rootFolder: TFolder | null;
focusedFolder: TFolder | null;
pinnedFolderPaths: string[];
focusedFile: TFile | null;
folderSortRule: FolderSortRule;
fileSortRule: FileSortRule;
@ -63,6 +71,10 @@ export type FileTreeStore = {
changeExpandedFolderPaths: (folderNames: string[]) => Promise<void>;
restoreExpandedFolderPaths: () => Promise<void>;
restoreLastFocusedFolder: () => Promise<void>;
pinFolder: (folder: TFolder) => Promise<void>;
unpinFolder: (folder: TFolder) => Promise<void>;
isFolderPinned: (folder: TFolder) => boolean;
restorePinnedFolders: () => Promise<void>;
// Files related
findFileByPath: (path: string) => TFile | null;
@ -84,6 +96,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
folders: plugin.app.vault.getAllFolders() || [],
rootFolder: plugin.app.vault.getRoot() || null,
focusedFolder: null,
pinnedFolderPaths: [],
focusedFile: null,
folderSortRule: DEFAULT_FOLDER_SORT_RULE,
fileSortRule: DEFAULT_FILE_SORT_RULE,
@ -98,7 +111,6 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
},
getData: async <T>(key: string): Promise<T | undefined> => {
const data = await plugin.loadData();
return data[key];
},
restoreData: async () => {
@ -108,6 +120,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
restoreFolderSortRule,
restoreFileSortRule,
restoreExpandedFolderPaths,
restorePinnedFolders,
} = get();
await Promise.all([
restoreLastFocusedFolder(),
@ -115,6 +128,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
restoreFolderSortRule(),
restoreFileSortRule(),
restoreExpandedFolderPaths(),
restorePinnedFolders(),
]);
},
@ -299,6 +313,48 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
_setFocusedFolder(rootFolder);
}
},
isFolderPinned: (folder: TFolder) => {
const { pinnedFolderPaths } = get();
return pinnedFolderPaths.includes(folder.path);
},
pinFolder: async (folder: TFolder) => {
const { pinnedFolderPaths, saveData } = get();
const folderPaths = [...pinnedFolderPaths, folder.path];
set({
pinnedFolderPaths: folderPaths,
});
await saveData({
[FFS_PINNED_FOLDER_PATHS_KEY]: JSON.stringify(folderPaths),
});
},
unpinFolder: async (folder: TFolder) => {
const { pinnedFolderPaths, saveData } = get();
const folderPaths = pinnedFolderPaths.filter(
(path) => path !== folder.path
);
set({
pinnedFolderPaths: folderPaths,
});
await saveData({
[FFS_PINNED_FOLDER_PATHS_KEY]: JSON.stringify(folderPaths),
});
},
restorePinnedFolders: async () => {
const { getData } = get();
const pinnedFolderPaths = await getData<string>(
FFS_PINNED_FOLDER_PATHS_KEY
);
if (pinnedFolderPaths) {
try {
const folderPaths: string[] = JSON.parse(pinnedFolderPaths);
set({
pinnedFolderPaths: folderPaths,
});
} catch (error) {
console.error("Invalid Json format: ", error);
}
}
},
// Files related
findFileByPath: (path: string): TFile | null => {

View file

@ -324,3 +324,32 @@ If your plugin does not need CSS, delete this file.
.ffs-actions-icon-wrapper:not(:last-child) {
margin-right: 12px;
}
.ffs-pinned-folders-section {
margin: 8px 0 4px;
padding: 6px 4px 8px;
background-color: var(--ffs-actions-background-color);
border-radius: 4px;
}
.ffs-pinned-title {
display: flex;
align-items: center;
font-weight: 500;
color: var(--ffs-actions-icon-color);
margin-bottom: 4px;
margin-left: 2px;
font-size: 12px;
}
.ffs-pinned-folders-section .ffs-pin-icon {
width: 12px;
height: 12px;
margin-right: 2px;
fill: var(--ffs-actions-icon-color);
color: var(--ffs-actions-icon-color);
}
.ffs-pinned-folders .ffs-folder-arrow-icon-wrapper {
width: 0;
}