From bb8633dacee2d0c53973f5c991e46b4a8f73eae9 Mon Sep 17 00:00:00 2001 From: aaron Date: Sat, 27 Jun 2026 15:41:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=93=E5=BC=80?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A1=B5=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=85=A8=E5=B1=80app=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增OpenTabsComponent组件,在日历视图底部展示当前打开的标签页 2. 为标签页列表添加拖拽、点击打开、悬浮预览等交互功能 3. 修复部分文件中直接使用全局app变量的问题,统一使用插件实例的app属性 4. 添加对应的CSS样式来布局日历和标签页区域 --- release/oz-calendar/styles.css | 38 +++++++ src/components/calendar.tsx | 56 ++++++----- src/components/openTabs.tsx | 179 +++++++++++++++++++++++++++++++++ src/main.ts | 2 +- src/settings/suggest.ts | 2 + src/settings/suggestor.ts | 2 + styles.css | 38 +++++++ 7 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 src/components/openTabs.tsx diff --git a/release/oz-calendar/styles.css b/release/oz-calendar/styles.css index 96761f4..8cb3c7c 100644 --- a/release/oz-calendar/styles.css +++ b/release/oz-calendar/styles.css @@ -327,3 +327,41 @@ settings: .oz-calendar-note-line-dragging { opacity: 0.5; } + +.oz-calendar-top-section { + display: flex; + flex-direction: column; + flex: 2 1 0; + min-height: 0; +} + +.oz-calendar-open-tabs-container { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + border-top: 1px solid var(--background-modifier-border); +} + +.oz-calendar-open-tabs-header { + padding: 6px 8px; + font-weight: 600; + font-size: var(--nav-item-size); + color: var(--oz-calendar-header-date-color); + flex-shrink: 0; +} + +.oz-calendar-open-tabs-list { + flex: 1 1 auto; + overflow-y: auto; + padding: 0 5px 5px 5px; + min-height: 0; +} + +.oz-calendar-plugin-view.fixed .oz-calendar-top-section { + overflow: hidden; +} + +.oz-calendar-plugin-view.fixed .oz-calendar-open-tabs-list { + overflow-y: auto; +} diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index b3aac8a..d3d2a0e 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -3,6 +3,7 @@ import Calendar, { CalendarTileProperties } from 'react-calendar'; import { RxDotFilled } from 'react-icons/rx'; import OZCalendarPlugin from '../main'; import NoteListComponent from './noteList'; +import OpenTabsComponent from './openTabs'; import dayjs from 'dayjs'; import useForceUpdate from 'hooks/forceUpdate'; import { DayChangeCommandAction } from 'types'; @@ -100,32 +101,32 @@ export default function MyCalendar(params: { plugin: OZCalendarPlugin }) { return (
- { - if (props.action === 'next') { - setActiveStartDate(dayjs(activeStartDate).add(1, 'month').toDate()); - } else if (props.action === 'next2') { - setActiveStartDate(dayjs(activeStartDate).add(12, 'month').toDate()); - } else if (props.action === 'prev') { - setActiveStartDate(dayjs(activeStartDate).add(-1, 'month').toDate()); - } else if (props.action === 'prev2') { - setActiveStartDate(dayjs(activeStartDate).add(-12, 'month').toDate()); - } - }} - formatMonthYear={(locale, date) => dayjs(date).format('MMM YYYY')} - /> - <> +
+ { + if (props.action === 'next') { + setActiveStartDate(dayjs(activeStartDate).add(1, 'month').toDate()); + } else if (props.action === 'next2') { + setActiveStartDate(dayjs(activeStartDate).add(12, 'month').toDate()); + } else if (props.action === 'prev') { + setActiveStartDate(dayjs(activeStartDate).add(-1, 'month').toDate()); + } else if (props.action === 'prev2') { + setActiveStartDate(dayjs(activeStartDate).add(-12, 'month').toDate()); + } + }} + formatMonthYear={(locale, date) => dayjs(date).format('MMM YYYY')} + />
- +
+
); } diff --git a/src/components/openTabs.tsx b/src/components/openTabs.tsx new file mode 100644 index 0000000..fec0fb8 --- /dev/null +++ b/src/components/openTabs.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { TFile } from 'obsidian'; +import { HiOutlineDocumentText } from 'react-icons/hi'; +import OZCalendarPlugin from '../main'; +import { openFile } from '../util/utils'; + +interface OpenTabNote { + displayName: string; + path: string; +} + +interface OpenTabsComponentParams { + plugin: OZCalendarPlugin; +} + +export default function OpenTabsComponent(params: OpenTabsComponentParams) { + const { plugin } = params; + + const getOpenTabNotes = (): OpenTabNote[] => { + const seen = new Set(); + const notes: OpenTabNote[] = []; + plugin.app.workspace.iterateAllLeaves((leaf) => { + const state = leaf.getViewState(); + if (state.type !== 'markdown') return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const file: TFile | undefined = (leaf.view as any)?.file; + if (!file || seen.has(file.path)) return; + seen.add(file.path); + notes.push({ displayName: file.basename, path: file.path }); + }); + return notes; + }; + + const [openTabs, setOpenTabs] = useState(getOpenTabNotes()); + + useEffect(() => { + const update = () => setOpenTabs(getOpenTabNotes()); + const refs = [ + plugin.app.workspace.on('layout-change', update), + plugin.app.workspace.on('active-leaf-change', update), + plugin.app.workspace.on('file-open', update), + ]; + return () => { + refs.forEach((ref) => plugin.app.workspace.offref(ref)); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const openFilePath = (e: React.MouseEvent, filePath: string) => { + const abstractFile = plugin.app.vault.getAbstractFileByPath(filePath); + const openFileBehaviour = plugin.settings.openFileBehaviour; + if (abstractFile && abstractFile instanceof TFile) { + let openInNewLeaf: boolean = openFileBehaviour === 'new-tab'; + let openInNewTabGroup: boolean = openFileBehaviour === 'new-tab-group'; + if (openFileBehaviour === 'obsidian-default') { + openInNewLeaf = (e.ctrlKey || e.metaKey) && !(e.shiftKey || e.altKey); + openInNewTabGroup = (e.ctrlKey || e.metaKey) && (e.shiftKey || e.altKey); + } + openFile({ + file: abstractFile, + plugin: plugin, + newLeaf: openInNewLeaf, + leafBySplit: openInNewTabGroup, + }); + } + }; + + const handleDragStart = (e: React.DragEvent, note: OpenTabNote) => { + // Use Obsidian's internal DragManager so the editor's drop handler + // recognizes the drag source. Standard HTML5 dataTransfer types alone + // (text/plain, text/uri-list) make the browser show the "not allowed" + // cursor when dragging into the editor; the editor's drop handler only + // preventDefault()s on dragover for drag payloads that went through + // dragManager.onDragStart. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dragManager = (plugin.app as any).dragManager; + if (dragManager && typeof dragManager.dragLink === 'function') { + const wikiLink = `[[${note.displayName}]]`; + const dragData = dragManager.dragLink(e.nativeEvent, wikiLink, note.path); + dragManager.onDragStart(e.nativeEvent, dragData); + } else { + const obsidianUri = `obsidian://open?vault=${encodeURIComponent( + plugin.app.vault.getName() + )}&file=${encodeURIComponent(note.path)}`; + e.dataTransfer.effectAllowed = 'link'; + e.dataTransfer.setData('text/uri-list', obsidianUri); + e.dataTransfer.setData('text/plain', `[[${note.displayName}]]`); + } + e.currentTarget.classList.add('oz-calendar-note-line-dragging'); + }; + + const handleDragEnd = (e: React.DragEvent) => { + e.currentTarget.classList.remove('oz-calendar-note-line-dragging'); + }; + + // Hover preview (Ctrl/Cmd pressed -> native HoverPopover, no delay). + const isModPressed = (e: React.MouseEvent): boolean => e.ctrlKey || e.metaKey; + + const activeTargetRef = useRef(null); + + const forceCloseHoverPreview = () => { + const targetEl = activeTargetRef.current; + activeTargetRef.current = null; + if (targetEl) { + targetEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false })); + } + document.querySelectorAll('.popover.hover-popover').forEach((el) => { + el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false })); + }); + }; + + const handleNoteMouseEnter = (e: React.MouseEvent, note: OpenTabNote) => { + if (!isModPressed(e)) return; + forceCloseHoverPreview(); + activeTargetRef.current = e.currentTarget; + plugin.app.workspace.trigger('hover-link', { + event: e.nativeEvent, + source: 'oz-calendar', + hoverParent: { hoverPopover: null }, + targetEl: e.currentTarget, + linktext: note.path, + sourcePath: note.path, + }); + }; + + const handleNoteMouseLeave = () => { + if (activeTargetRef.current) { + forceCloseHoverPreview(); + } + }; + + useEffect(() => { + const onKeyUp = (ev: KeyboardEvent) => { + if (!ev.ctrlKey && !ev.metaKey) { + forceCloseHoverPreview(); + } + }; + const onBlur = () => forceCloseHoverPreview(); + window.addEventListener('keyup', onKeyUp); + window.addEventListener('blur', onBlur); + return () => { + window.removeEventListener('keyup', onKeyUp); + window.removeEventListener('blur', onBlur); + forceCloseHoverPreview(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
Open tabs
+
+ {openTabs.length === 0 && ( +
No open tabs
+ )} + {openTabs.map((note) => ( +
openFilePath(e, note.path)} + onMouseEnter={(e) => handleNoteMouseEnter(e, note)} + onMouseLeave={handleNoteMouseLeave} + onDragStart={(e) => handleDragStart(e, note)} + onDragEnd={handleDragEnd}> + + {note.displayName} +
+ ))} +
+
+ ); +} diff --git a/src/main.ts b/src/main.ts index 47c1cbb..525d5f1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -323,7 +323,7 @@ export default class OZCalendarPlugin extends Plugin { for (let mdFile of mdFiles) { if (this.settings.dateSource === 'yaml') { // Get the file Cache - let fileCache = app.metadataCache.getFileCache(mdFile); + let fileCache = this.app.metadataCache.getFileCache(mdFile); // Check if there is Frontmatter if (fileCache && fileCache.frontmatter) { let fm = fileCache.frontmatter; diff --git a/src/settings/suggest.ts b/src/settings/suggest.ts index a930900..3af1004 100644 --- a/src/settings/suggest.ts +++ b/src/settings/suggest.ts @@ -3,6 +3,8 @@ import { ISuggestOwner, Scope } from 'obsidian'; import { createPopper, Instance as PopperInstance } from '@popperjs/core'; +declare const app: any; + const wrapAround = (value: number, size: number): number => { return ((value % size) + size) % size; }; diff --git a/src/settings/suggestor.ts b/src/settings/suggestor.ts index b4c9df0..905be7e 100644 --- a/src/settings/suggestor.ts +++ b/src/settings/suggestor.ts @@ -3,6 +3,8 @@ import { TAbstractFile, TFolder } from 'obsidian'; import { TextInputSuggest } from './suggest'; +declare const app: any; + export class FolderSuggest extends TextInputSuggest { getSuggestions(inputStr: string): TFolder[] { const abstractFiles = app.vault.getAllLoadedFiles(); diff --git a/styles.css b/styles.css index 96761f4..8cb3c7c 100644 --- a/styles.css +++ b/styles.css @@ -327,3 +327,41 @@ settings: .oz-calendar-note-line-dragging { opacity: 0.5; } + +.oz-calendar-top-section { + display: flex; + flex-direction: column; + flex: 2 1 0; + min-height: 0; +} + +.oz-calendar-open-tabs-container { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + border-top: 1px solid var(--background-modifier-border); +} + +.oz-calendar-open-tabs-header { + padding: 6px 8px; + font-weight: 600; + font-size: var(--nav-item-size); + color: var(--oz-calendar-header-date-color); + flex-shrink: 0; +} + +.oz-calendar-open-tabs-list { + flex: 1 1 auto; + overflow-y: auto; + padding: 0 5px 5px 5px; + min-height: 0; +} + +.oz-calendar-plugin-view.fixed .oz-calendar-top-section { + overflow: hidden; +} + +.oz-calendar-plugin-view.fixed .oz-calendar-open-tabs-list { + overflow-y: auto; +}