mirror of
https://github.com/xuquan-nikkkki/FolderFile-Splitter-Plugin.git
synced 2026-07-22 05:37:28 +00:00
feat: support pin files
This commit is contained in:
parent
e59cd6489d
commit
18490d8f11
5 changed files with 128 additions and 27 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
59
src/store.ts
59
src/store.ts
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
14
styles.css
14
styles.css
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue