From 18490d8f11cdcba46de0ea6adbc37010f4049b4c Mon Sep 17 00:00:00 2001 From: Xu Quan Date: Wed, 5 Mar 2025 18:42:55 +0800 Subject: [PATCH] feat: support pin files --- src/assets/constants.ts | 1 + src/components/File.tsx | 26 ++++++++++++++++-- src/components/Files.tsx | 55 +++++++++++++++++++++++++++---------- src/store.ts | 59 +++++++++++++++++++++++++++++++++++----- styles.css | 14 ++++++++-- 5 files changed, 128 insertions(+), 27 deletions(-) diff --git a/src/assets/constants.ts b/src/assets/constants.ts index 0600fde..a911e0e 100644 --- a/src/assets/constants.ts +++ b/src/assets/constants.ts @@ -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"; diff --git a/src/components/File.tsx b/src/components/File.tsx index 07cc2d5..02d7794 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -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 ; }; - 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 = () => { diff --git a/src/components/Files.tsx b/src/components/Files.tsx index 9d33cc5..8617395 100644 --- a/src/components/Files.tsx +++ b/src/components/Files.tsx @@ -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>; @@ -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) => ( + 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 ( +
+ + + Pin + +
+ {pinnedFiles.map(renderFile)} +
+
+ ); + }; + if (!files.length) return renderNoneFilesTips(); const sortedFiles = sortFiles(files, fileSortRule); return ( -
- {sortedFiles.map((file) => ( - onDeleteFileFromList(file)} - fileList={sortedFiles} - selectedFiles={selectedFiles} - setSelectedFiles={setSelectedFiles} - draggingFiles={draggingFiles} - setDraggingFiles={setDraggingFiles} - /> - ))} +
+ {renderPinnedFiles()} +
+ {sortedFiles.map(renderFile)} +
); }; diff --git a/src/store.ts b/src/store.ts index f569c1e..bc486c3 100644 --- a/src/store.ts +++ b/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; restoreFileSortRule: () => Promise; sortFiles: (files: TFile[], rule: FileSortRule) => TFile[]; + pinFile: (file: TFile) => Promise; + unpinFile: (file: TFile) => Promise; + isFilePinned: (file: TFile) => boolean; + restorePinnedFiles: () => Promise; }; 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( + 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); + } + } + }, })); diff --git a/styles.css b/styles.css index 04fbc68..2eb63ab 100644 --- a/styles.css +++ b/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;