feat: support pin files

This commit is contained in:
Xu Quan 2025-03-05 18:42:55 +08:00
parent e59cd6489d
commit 18490d8f11
5 changed files with 128 additions and 27 deletions

View file

@ -6,6 +6,7 @@ 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_PINNED_FILE_PATHS_KEY = "PinnedFilePaths";
export const FFS_FOLDER_PANE_WIDTH_KEY =
"FolderFileSplitterPlugin-FolderPaneWidth";

View file

@ -41,6 +41,9 @@ const File = ({
duplicateFile,
folders,
setFocusedFile,
isFilePinned,
pinFile,
unpinFile,
} = useFileTreeStore(
useShallow((store: FileTreeStore) => ({
focusedFile: store.focusedFile,
@ -49,6 +52,9 @@ const File = ({
duplicateFile: store.duplicateFile,
folders: store.folders,
setFocusedFile: store.setFocusedFile,
isFilePinned: store.isFilePinned,
pinFile: store.pinFile,
unpinFile: store.unpinFile,
}))
);
@ -86,6 +92,19 @@ const File = ({
e.stopPropagation();
const menu = new Menu();
menu.addItem((item) => {
const isPinned = isFilePinned(file);
const title = isPinned ? "Unpin file" : "Pin file";
item.setTitle(title);
item.onClick(() => {
if (isPinned) {
unpinFile(file);
} else {
pinFile(file);
}
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle("Open in new tab");
item.onClick(() => {
@ -140,17 +159,18 @@ const File = ({
return <FileDetail useFileTreeStore={useFileTreeStore} file={file} />;
};
const _isFileSelected = (fi: TFile) => isAbstractFileIncluded(selectedFiles, fi);
const _isFileSelected = (fi: TFile) =>
isAbstractFileIncluded(selectedFiles, fi);
const isFileSelected = () => _isFileSelected(file);
const beginMultiSelect = (): TFile[] => {
setFocusedFile(null);
let newFiles = [...selectedFiles]
let newFiles = [...selectedFiles];
if (focusedFile && !_isFileSelected(focusedFile)) {
newFiles = [...selectedFiles, focusedFile];
}
return newFiles
return newFiles;
};
const onSelectRange = () => {

View file

@ -9,6 +9,7 @@ import File from "./File";
import FolderFileSplitterPlugin from "src/main";
import { useChangeFile } from "src/hooks/useVaultChangeHandler";
import { TFile } from "obsidian";
import PinIcon from "src/assets/icons/PinIcon";
type Props = {
useFileTreeStore: UseBoundStore<StoreApi<FileTreeStore>>;
@ -20,6 +21,7 @@ const Files = ({ useFileTreeStore, plugin }: Props) => {
sortFiles: store.sortFiles,
fileSortRule: store.fileSortRule,
focusedFile: store.focusedFile,
pinnedFiles: store.pinnedFilePaths
}))
);
const { files, onDeleteFileFromList } = useChangeFile({ useFileTreeStore });
@ -49,24 +51,47 @@ const Files = ({ useFileTreeStore, plugin }: Props) => {
);
};
const renderFile = (file: TFile) => (
<File
key={file.name}
useFileTreeStore={useFileTreeStore}
file={file}
plugin={plugin}
deleteFile={() => onDeleteFileFromList(file)}
fileList={sortedFiles}
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
draggingFiles={draggingFiles}
setDraggingFiles={setDraggingFiles}
/>
)
const renderPinnedFiles = () => {
const pinnedFilePaths = useFileTreeStore.getState().pinnedFilePaths;
const pinnedFiles = files.filter(f => pinnedFilePaths.includes(f.path))
if (!pinnedFiles) return null;
return (
<div className="ffs-pinned-files-section">
<span className="ffs-pinned-title">
<PinIcon />
Pin
</span>
<div className="ffs-pinned-files">
{pinnedFiles.map(renderFile)}
</div>
</div>
);
};
if (!files.length) return renderNoneFilesTips();
const sortedFiles = sortFiles(files, fileSortRule);
return (
<div ref={filesRef}>
{sortedFiles.map((file) => (
<File
key={file.name}
useFileTreeStore={useFileTreeStore}
file={file}
plugin={plugin}
deleteFile={() => onDeleteFileFromList(file)}
fileList={sortedFiles}
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
draggingFiles={draggingFiles}
setDraggingFiles={setDraggingFiles}
/>
))}
<div>
{renderPinnedFiles()}
<div ref={filesRef}>
{sortedFiles.map(renderFile)}
</div>
</div>
);
};

View file

@ -2,19 +2,14 @@ import { create } from "zustand";
import { TFile, TFolder } from "obsidian";
import FolderFileSplitterPlugin from "./main";
import {
addItem,
isAbstractFileIncluded,
isFile,
isFolder,
removeItem,
} from "./utils";
import { isAbstractFileIncluded, isFile, isFolder } 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_FILE_PATHS_KEY,
FFS_PINNED_FOLDER_PATHS_KEY,
} from "./assets/constants";
@ -39,6 +34,7 @@ export type FileTreeStore = {
focusedFolder: TFolder | null;
pinnedFolderPaths: string[];
focusedFile: TFile | null;
pinnedFilePaths: string[];
folderSortRule: FolderSortRule;
fileSortRule: FileSortRule;
expandedFolderPaths: string[];
@ -89,6 +85,10 @@ export type FileTreeStore = {
changeFileSortRule: (rule: FileSortRule) => Promise<void>;
restoreFileSortRule: () => Promise<void>;
sortFiles: (files: TFile[], rule: FileSortRule) => TFile[];
pinFile: (file: TFile) => Promise<void>;
unpinFile: (file: TFile) => Promise<void>;
isFilePinned: (file: TFile) => boolean;
restorePinnedFiles: () => Promise<void>;
};
export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
@ -98,6 +98,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
focusedFolder: null,
pinnedFolderPaths: [],
focusedFile: null,
pinnedFilePaths: [],
folderSortRule: DEFAULT_FOLDER_SORT_RULE,
fileSortRule: DEFAULT_FILE_SORT_RULE,
expandedFolderPaths: [],
@ -121,6 +122,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
restoreFileSortRule,
restoreExpandedFolderPaths,
restorePinnedFolders,
restorePinnedFiles
} = get();
await Promise.all([
restoreLastFocusedFolder(),
@ -129,6 +131,7 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
restoreFileSortRule(),
restoreExpandedFolderPaths(),
restorePinnedFolders(),
restorePinnedFiles(),
]);
},
@ -480,4 +483,46 @@ export const createFileTreeStore = (plugin: FolderFileSplitterPlugin) =>
});
}
},
isFilePinned: (file: TFile) => {
const { pinnedFilePaths } = get();
return pinnedFilePaths.includes(file.path);
},
pinFile: async (file: TFile) => {
const { pinnedFilePaths, saveData } = get();
const filePaths = [...pinnedFilePaths, file.path];
set({
pinnedFilePaths: filePaths,
});
await saveData({
[FFS_PINNED_FILE_PATHS_KEY]: JSON.stringify(filePaths),
});
},
unpinFile: async (file: TFile) => {
const { pinnedFilePaths, saveData } = get();
const filePaths = pinnedFilePaths.filter(
(path) => path !== file.path
);
set({
pinnedFilePaths: filePaths,
});
await saveData({
[FFS_PINNED_FILE_PATHS_KEY]: JSON.stringify(filePaths),
});
},
restorePinnedFiles: async () => {
const { getData } = get();
const pinnedFilePaths = await getData<string>(
FFS_PINNED_FILE_PATHS_KEY
);
if (pinnedFilePaths) {
try {
const filePaths: string[] = JSON.parse(pinnedFilePaths);
set({
pinnedFilePaths: filePaths,
});
} catch (error) {
console.error("Invalid Json format: ", error);
}
}
},
}));

View file

@ -325,13 +325,22 @@ If your plugin does not need CSS, delete this file.
margin-right: 12px;
}
.ffs-pinned-folders-section {
.ffs-pinned-folders-section,
.ffs-pinned-files-section {
margin: 8px 0 4px;
padding: 6px 4px 8px;
background-color: var(--ffs-actions-background-color);
border-radius: 4px;
}
.ffs-pinned-files-section {
margin-left: 12px;
}
.ffs-pinned-files-section .ffs-file {
margin-left: 0;
}
.ffs-pinned-title {
display: flex;
align-items: center;
@ -342,7 +351,8 @@ If your plugin does not need CSS, delete this file.
font-size: 12px;
}
.ffs-pinned-folders-section .ffs-pin-icon {
.ffs-pinned-folders-section .ffs-pin-icon,
.ffs-pinned-files-section .ffs-pin-icon {
width: 12px;
height: 12px;
margin-right: 2px;