mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
feat: integrate toolbar v1
This commit is contained in:
parent
6502bdcf7c
commit
75a07a6ca5
5 changed files with 411 additions and 0 deletions
99
components/Toolbar/EditorToolbarButton.tsx
Normal file
99
components/Toolbar/EditorToolbarButton.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import React, { FC, memo, useEffect, useState } from "react";
|
||||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
interface EditorToolbarButtonProps {
|
||||
statuses: GroupedStatuses;
|
||||
onClick: () => void;
|
||||
unknownStatusConfig: {
|
||||
icon: string;
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
||||
({ statuses, onClick, unknownStatusConfig }) => {
|
||||
const [statusChanged, setStatusChanged] = useState(false);
|
||||
const statusEntries = Object.entries(statuses);
|
||||
const allStatuses = statusEntries.flatMap(
|
||||
([_, statusList]) => statusList,
|
||||
);
|
||||
const totalStatuses = allStatuses.length;
|
||||
// Animate when status changes
|
||||
useEffect(() => {
|
||||
setStatusChanged(true);
|
||||
const timer = setTimeout(() => setStatusChanged(false), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [statusEntries.length, allStatuses.map((s) => s.name).join(",")]);
|
||||
|
||||
const getTooltipText = () => {
|
||||
if (totalStatuses === 0) {
|
||||
return "No status assigned - click to set status";
|
||||
}
|
||||
if (totalStatuses === 1) {
|
||||
return `Status: ${allStatuses[0].name} - click to change`;
|
||||
}
|
||||
const statusNames = allStatuses.map((s) => s.name).join(", ");
|
||||
return `Statuses: ${statusNames} - click to change`;
|
||||
};
|
||||
|
||||
const buttonClasses = [
|
||||
"editor-toolbar-btn",
|
||||
totalStatuses === 0
|
||||
? "editor-toolbar-btn--no-status"
|
||||
: "editor-toolbar-btn--has-status",
|
||||
statusChanged ? "editor-toolbar-btn--status-changed" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
if (totalStatuses === 0) {
|
||||
return (
|
||||
<button
|
||||
className={buttonClasses}
|
||||
onClick={onClick}
|
||||
title={getTooltipText()}
|
||||
aria-label={getTooltipText()}
|
||||
style={
|
||||
{
|
||||
"--status-color": unknownStatusConfig.color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="editor-toolbar-btn__icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{unknownStatusConfig.icon}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const primaryStatus = allStatuses[0];
|
||||
|
||||
return (
|
||||
<button
|
||||
className={buttonClasses}
|
||||
onClick={onClick}
|
||||
title={getTooltipText()}
|
||||
aria-label={getTooltipText()}
|
||||
style={
|
||||
{
|
||||
"--status-color":
|
||||
primaryStatus.color || "var(--interactive-accent)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="editor-toolbar-btn__icon" aria-hidden="true">
|
||||
{primaryStatus.icon || "📝"}
|
||||
</span>
|
||||
{totalStatuses > 1 && (
|
||||
<span
|
||||
className="editor-toolbar-btn__count"
|
||||
aria-label={`${totalStatuses} statuses`}
|
||||
>
|
||||
{totalStatuses}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
218
integrations/toolbar/editorToolbarIntegration.tsx
Normal file
218
integrations/toolbar/editorToolbarIntegration.tsx
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { createRoot, Root } from "react-dom/client";
|
||||
import eventBus from "core/eventBus";
|
||||
import { MarkdownView, Plugin, WorkspaceLeaf } from "obsidian";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { EditorToolbarButton } from "@/components/Toolbar/EditorToolbarButton";
|
||||
import { NoteStatusService } from "@/core/noteStatusService";
|
||||
|
||||
export class EditorToolbarIntegration {
|
||||
private static instance: EditorToolbarIntegration | null = null;
|
||||
private root: Root | null = null;
|
||||
private plugin: Plugin;
|
||||
private buttonElement: HTMLElement | null = null;
|
||||
private noteStatusService: NoteStatusService | null = null;
|
||||
private currentLeaf: WorkspaceLeaf | null = null;
|
||||
private readonly BUTTON_CLASS = "note-status-editor-toolbar-button";
|
||||
|
||||
constructor(plugin: Plugin) {
|
||||
if (EditorToolbarIntegration.instance) {
|
||||
throw new Error("The editor toolbar instance is already created");
|
||||
}
|
||||
this.plugin = plugin;
|
||||
EditorToolbarIntegration.instance = this;
|
||||
}
|
||||
|
||||
async integrate() {
|
||||
eventBus.subscribe(
|
||||
"active-file-change",
|
||||
({ leaf }) => {
|
||||
if (this.isValidMarkdownLeaf(leaf)) {
|
||||
this.currentLeaf = leaf;
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
} else {
|
||||
this.removeButton();
|
||||
}
|
||||
},
|
||||
"editorToolbarIntegrationSubscription1",
|
||||
);
|
||||
|
||||
eventBus.subscribe(
|
||||
"plugin-settings-changed",
|
||||
({ key }) => {
|
||||
if (key === "enabledTemplates" || key === "templates") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (key === "useMultipleStatuses") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (key === "tagPrefix") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (key === "useCustomStatusesOnly") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (key === "customStatuses") {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
}
|
||||
if (
|
||||
key === "unknownStatusIcon" ||
|
||||
key === "unknownStatusColor"
|
||||
) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
"editorToolbarIntegrationSubscription2",
|
||||
);
|
||||
|
||||
eventBus.subscribe(
|
||||
"frontmatter-manually-changed",
|
||||
() => {
|
||||
this.handleActiveFileChange().catch(console.error);
|
||||
},
|
||||
"editorToolbarIntegrationSubscription3",
|
||||
);
|
||||
}
|
||||
|
||||
private async handleActiveFileChange() {
|
||||
this.extractStatusesFromLeaf(this.currentLeaf);
|
||||
this.createButton();
|
||||
this.render();
|
||||
}
|
||||
|
||||
private extractStatusesFromLeaf(leaf: WorkspaceLeaf | null) {
|
||||
if (!this.isValidMarkdownLeaf(leaf)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const markdownView = leaf!.view as MarkdownView;
|
||||
if (!markdownView.file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.noteStatusService = new NoteStatusService(markdownView.file);
|
||||
this.noteStatusService.populateStatuses();
|
||||
}
|
||||
|
||||
private isValidMarkdownLeaf(leaf: WorkspaceLeaf | null): boolean {
|
||||
return leaf !== null && leaf.view.getViewType() === "markdown";
|
||||
}
|
||||
|
||||
private createButton(): void {
|
||||
if (!this.isValidMarkdownLeaf(this.currentLeaf)) {
|
||||
return;
|
||||
}
|
||||
const markdownView = this.currentLeaf!.view as MarkdownView;
|
||||
|
||||
// Try multiple selectors for different Obsidian versions
|
||||
const possibleSelectors = [
|
||||
".view-header-nav-buttons",
|
||||
".view-actions",
|
||||
".view-header .clickable-icon:last-child",
|
||||
];
|
||||
|
||||
let actionsContainer: Element | null = null;
|
||||
|
||||
for (const selector of possibleSelectors) {
|
||||
actionsContainer = markdownView.containerEl.querySelector(selector);
|
||||
if (actionsContainer) break;
|
||||
}
|
||||
|
||||
// Fallback: create container if none exists
|
||||
if (!actionsContainer) {
|
||||
const viewHeader =
|
||||
markdownView.containerEl.querySelector(".view-header");
|
||||
if (viewHeader) {
|
||||
actionsContainer = document.createElement("div");
|
||||
actionsContainer.className = "view-header-nav-buttons";
|
||||
viewHeader.appendChild(actionsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!actionsContainer) {
|
||||
console.warn(
|
||||
"Could not find or create actions container for editor toolbar button",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing button if it exists
|
||||
if (this.buttonElement) {
|
||||
this.removeButton();
|
||||
}
|
||||
|
||||
this.buttonElement = document.createElement("div");
|
||||
this.buttonElement.className = this.BUTTON_CLASS;
|
||||
|
||||
// Insert button at the end of actions container (right side)
|
||||
if (actionsContainer) {
|
||||
actionsContainer.appendChild(this.buttonElement);
|
||||
}
|
||||
this.root = createRoot(this.buttonElement);
|
||||
}
|
||||
|
||||
private removeButton(): void {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
if (this.buttonElement) {
|
||||
this.buttonElement.remove();
|
||||
this.buttonElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
private openStatusModal() {
|
||||
if (!this.noteStatusService) {
|
||||
throw new Error(
|
||||
"open status modal failed because there is no noteStatusService available",
|
||||
);
|
||||
}
|
||||
eventBus.publish("triggered-open-modal", {
|
||||
statusService: this.noteStatusService,
|
||||
});
|
||||
}
|
||||
|
||||
private getUnknownStatusConfig() {
|
||||
const settings = settingsService.settings;
|
||||
return {
|
||||
icon: settings.unknownStatusIcon || "❓",
|
||||
color: settings.unknownStatusColor || "#8b949e",
|
||||
};
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.root || !this.buttonElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.root.render(
|
||||
<EditorToolbarButton
|
||||
statuses={this.noteStatusService?.statuses || {}}
|
||||
onClick={() => this.openStatusModal()}
|
||||
unknownStatusConfig={this.getUnknownStatusConfig()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
eventBus.unsubscribe(
|
||||
"active-file-change",
|
||||
"editorToolbarIntegrationSubscription1",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
"plugin-settings-changed",
|
||||
"editorToolbarIntegrationSubscription2",
|
||||
);
|
||||
eventBus.unsubscribe(
|
||||
"frontmatter-manually-changed",
|
||||
"editorToolbarIntegrationSubscription3",
|
||||
);
|
||||
|
||||
this.removeButton();
|
||||
this.currentLeaf = null;
|
||||
this.noteStatusService = null;
|
||||
EditorToolbarIntegration.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorToolbarIntegration;
|
||||
9
main.tsx
9
main.tsx
|
|
@ -8,6 +8,7 @@ import { StatusModalIntegration } from "./integrations/modals/statusModalIntegra
|
|||
import ContextMenuIntegration from "./integrations/context-menu/contextMenuIntegration";
|
||||
import { FileExplorerIntegration } from "./integrations/file-explorer/file-explorer-integration";
|
||||
import { CommandsIntegration } from "./integrations/commands/commandsIntegration";
|
||||
import EditorToolbarIntegration from "./integrations/toolbar/editorToolbarIntegration";
|
||||
import {
|
||||
GroupedStatusView,
|
||||
VIEW_TYPE_EXAMPLE,
|
||||
|
|
@ -23,6 +24,7 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
private contextMenuIntegration: ContextMenuIntegration;
|
||||
private fileExplorerIntegration: FileExplorerIntegration;
|
||||
private commandsIntegration: CommandsIntegration;
|
||||
private editorToolbarIntegration: EditorToolbarIntegration;
|
||||
|
||||
async onload() {
|
||||
BaseNoteStatusService.initialize(this.app);
|
||||
|
|
@ -34,6 +36,7 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
this.loadStatusBar(),
|
||||
this.loadFileExplorer(),
|
||||
this.loadCommands(),
|
||||
this.loadEditorToolbar(),
|
||||
this.loadEventBus(),
|
||||
]);
|
||||
|
||||
|
|
@ -62,6 +65,7 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
this.contextMenuIntegration?.destroy();
|
||||
this.fileExplorerIntegration?.destroy();
|
||||
this.commandsIntegration?.destroy();
|
||||
this.editorToolbarIntegration?.destroy();
|
||||
this.pluginSettingsIntegration?.destroy();
|
||||
|
||||
// Clean up event subscriptions
|
||||
|
|
@ -188,4 +192,9 @@ export default class NoteStatusPlugin extends Plugin {
|
|||
this.commandsIntegration = new CommandsIntegration(this);
|
||||
await this.commandsIntegration.integrate();
|
||||
}
|
||||
|
||||
private async loadEditorToolbar() {
|
||||
this.editorToolbarIntegration = new EditorToolbarIntegration(this);
|
||||
await this.editorToolbarIntegration.integrate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
styles/components/toolbar-button.css
Normal file
84
styles/components/toolbar-button.css
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/* Editor Toolbar Button - Status indicator for notes */
|
||||
.editor-toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--header-height);
|
||||
height: var(--header-height);
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--clickable-icon-radius);
|
||||
background: transparent;
|
||||
color: var(--icon-color);
|
||||
cursor: pointer;
|
||||
opacity: var(--icon-opacity);
|
||||
position: relative;
|
||||
transition: var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.editor-toolbar-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
opacity: var(--icon-opacity-hover);
|
||||
}
|
||||
|
||||
.editor-toolbar-btn:active {
|
||||
background: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.editor-toolbar-btn:focus-visible {
|
||||
outline: 2px solid var(--background-modifier-border-focus);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.editor-toolbar-btn__icon {
|
||||
font-size: var(--icon-size-sm);
|
||||
line-height: 1;
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
.editor-toolbar-btn__count {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
min-width: var(--size-2-3);
|
||||
height: var(--size-2-3);
|
||||
padding: 0 var(--size-2-1);
|
||||
background: var(--status-color, var(--interactive-accent));
|
||||
color: var(--text-on-accent);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--radius-xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--background-primary);
|
||||
}
|
||||
|
||||
/* Status variants */
|
||||
.editor-toolbar-btn--no-status .editor-toolbar-btn__icon {
|
||||
color: var(--status-color, var(--text-muted));
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.editor-toolbar-btn--has-status .editor-toolbar-btn__icon {
|
||||
color: var(--status-color, var(--interactive-accent));
|
||||
}
|
||||
|
||||
.editor-toolbar-btn--has-status:hover .editor-toolbar-btn__icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Status change animation */
|
||||
@keyframes status-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.editor-toolbar-btn--status-changed .editor-toolbar-btn__icon {
|
||||
animation: status-pulse var(--anim-duration-moderate) ease-out;
|
||||
}
|
||||
|
|
@ -16,3 +16,4 @@
|
|||
@import "components/collapsible-counter.css";
|
||||
@import "components/status-display.css";
|
||||
@import "components/obsidian-icon.css";
|
||||
@import "components/toolbar-button.css";
|
||||
|
|
|
|||
Loading…
Reference in a new issue