feat: 添加打开标签页列表功能并修复全局app引用问题

1.  新增OpenTabsComponent组件,在日历视图底部展示当前打开的标签页
2.  为标签页列表添加拖拽、点击打开、悬浮预览等交互功能
3.  修复部分文件中直接使用全局app变量的问题,统一使用插件实例的app属性
4.  添加对应的CSS样式来布局日历和标签页区域
This commit is contained in:
aaron 2026-06-27 15:41:33 +08:00
parent 897c0890ca
commit bb8633dace
7 changed files with 289 additions and 28 deletions

View file

@ -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;
}

View file

@ -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 (
<div className={'oz-calendar-plugin-view ' + fixedCalendarClass}>
<Calendar
onChange={setSelectedDay}
value={selectedDay}
maxDetail="month"
minDetail="month"
showWeekNumbers={plugin.settings.showWeekNumbers}
view="month"
tileContent={customTileContent}
tileClassName={customTileClass}
calendarType={plugin.settings.calendarType}
showFixedNumberOfWeeks={plugin.settings.fixedCalendar}
activeStartDate={activeStartDate}
onActiveStartDateChange={(props) => {
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')}
/>
<>
<div className="oz-calendar-top-section">
<Calendar
onChange={setSelectedDay}
value={selectedDay}
maxDetail="month"
minDetail="month"
showWeekNumbers={plugin.settings.showWeekNumbers}
view="month"
tileContent={customTileContent}
tileClassName={customTileClass}
calendarType={plugin.settings.calendarType}
showFixedNumberOfWeeks={plugin.settings.fixedCalendar}
activeStartDate={activeStartDate}
onActiveStartDateChange={(props) => {
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')}
/>
<div id="oz-calendar-divider"></div>
<NoteListComponent
selectedDay={selectedDay}
@ -135,7 +136,8 @@ export default function MyCalendar(params: { plugin: OZCalendarPlugin }) {
forceValue={forceValue}
createNote={createNote}
/>
</>
</div>
<OpenTabsComponent plugin={plugin} />
</div>
);
}

179
src/components/openTabs.tsx Normal file
View file

@ -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<string>();
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<OpenTabNote[]>(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<HTMLDivElement, 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<HTMLDivElement>, 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<HTMLDivElement>) => {
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<HTMLElement | null>(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<HTMLDivElement>, 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 (
<div className="oz-calendar-open-tabs-container">
<div className="oz-calendar-open-tabs-header">Open tabs</div>
<div className="oz-calendar-open-tabs-list">
{openTabs.length === 0 && (
<div className="oz-calendar-note-no-note">No open tabs</div>
)}
{openTabs.map((note) => (
<div
className={
'oz-calendar-note-line' +
(plugin.settings.fileNameOverflowBehaviour == 'hide'
? ' oz-calendar-overflow-hide'
: '')
}
id={`oz-open-tab-${note.path}`}
key={note.path}
draggable={true}
onClick={(e) => openFilePath(e, note.path)}
onMouseEnter={(e) => handleNoteMouseEnter(e, note)}
onMouseLeave={handleNoteMouseLeave}
onDragStart={(e) => handleDragStart(e, note)}
onDragEnd={handleDragEnd}>
<HiOutlineDocumentText className="oz-calendar-note-line-icon" />
<span>{note.displayName}</span>
</div>
))}
</div>
</div>
);
}

View file

@ -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;

View file

@ -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;
};

View file

@ -3,6 +3,8 @@
import { TAbstractFile, TFolder } from 'obsidian';
import { TextInputSuggest } from './suggest';
declare const app: any;
export class FolderSuggest extends TextInputSuggest<TFolder> {
getSuggestions(inputStr: string): TFolder[] {
const abstractFiles = app.vault.getAllLoadedFiles();

View file

@ -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;
}