feat(fluent): add custom button support to top navigation

Extend FluentTopNavigation to support dynamic custom buttons that can
be registered by views or components. Features include:

- CustomNavButton interface for button configuration
- Custom buttons container in navigation bar
- Register/unregister methods for button management
- Integration with FluentComponentManager
- Styling for custom button container
This commit is contained in:
Quorafind 2025-11-13 11:38:22 +08:00
parent bcd3f2bf95
commit 2177d9ad31
4 changed files with 138 additions and 1 deletions

View file

@ -6,6 +6,7 @@ import {
Platform,
Component,
TFile,
ExtraButtonComponent,
} from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
@ -14,6 +15,13 @@ import { Events, on } from "@/dataflow/events/Events";
export type ViewMode = "list" | "kanban" | "tree" | "calendar";
export interface CustomNavButton {
id: string;
icon: string;
tooltip: string;
onClick: () => void;
}
export function isCompletedMark(
plugin: TaskProgressBarPlugin,
mark: string
@ -39,6 +47,8 @@ export class TopNavigation extends Component {
private notificationCount = 0;
private availableModes: ViewMode[] = ["list", "kanban", "tree", "calendar"];
private viewTabsContainer: HTMLElement | null = null;
private customButtonsContainer: HTMLElement | null = null;
private customButtons: Map<string, { buttonEl: HTMLElement; config: CustomNavButton }> = new Map();
constructor(
containerEl: HTMLElement,
@ -197,6 +207,11 @@ export class TopNavigation extends Component {
cls: "fluent-nav-right",
});
// Custom buttons container (for dynamic buttons from views)
this.customButtonsContainer = rightSection.createDiv({
cls: "fluent-nav-custom-buttons",
});
// Notification button
const notificationBtn = rightSection.createDiv({
cls: "fluent-nav-icon-button",
@ -422,4 +437,76 @@ export class TopNavigation extends Component {
public getCurrentViewMode(): ViewMode {
return this.currentViewMode;
}
/**
* Register a custom button in the top navigation
*/
public registerCustomButton(config: CustomNavButton): void {
if (!this.customButtonsContainer) {
console.warn("[FluentTopNavigation] Custom buttons container not initialized");
return;
}
// Check if button already exists
if (this.customButtons.has(config.id)) {
console.warn(`[FluentTopNavigation] Button with id "${config.id}" already registered`);
return;
}
// Create button container
const buttonContainer = this.customButtonsContainer.createDiv({
cls: "fluent-nav-custom-button-wrapper",
});
// Create button using ExtraButtonComponent
const button = new ExtraButtonComponent(buttonContainer)
.setIcon(config.icon)
.setTooltip(config.tooltip)
.onClick(config.onClick);
// Store reference
this.customButtons.set(config.id, {
buttonEl: buttonContainer,
config: config,
});
console.log(`[FluentTopNavigation] Registered custom button: ${config.id}`);
}
/**
* Unregister a custom button by ID
*/
public unregisterCustomButton(id: string): void {
const button = this.customButtons.get(id);
if (!button) {
return;
}
// Remove DOM element
button.buttonEl.remove();
// Remove from map
this.customButtons.delete(id);
console.log(`[FluentTopNavigation] Unregistered custom button: ${id}`);
}
/**
* Clear all custom buttons
*/
public clearCustomButtons(): void {
if (!this.customButtonsContainer) {
return;
}
// Remove all button elements
this.customButtons.forEach((button) => {
button.buttonEl.remove();
});
// Clear map
this.customButtons.clear();
console.log("[FluentTopNavigation] Cleared all custom buttons");
}
}

View file

@ -20,7 +20,7 @@ import { filterTasks } from "@/utils/task/task-filter-utils";
import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilter";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
import { t } from "@/translations/helper";
import { ViewMode } from "../components/FluentTopNavigation";
import { ViewMode, TopNavigation } from "../components/FluentTopNavigation";
import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager";
type ManagedViewComponent = {
@ -85,6 +85,9 @@ export class FluentComponentManager extends Component {
// Track currently visible component
private currentVisibleComponent: ManagedViewComponent | null = null;
// Top Navigation reference (for registering custom buttons)
private topNavigation: TopNavigation | null = null;
// View handlers
private viewHandlers: {
onTaskSelected: (task: Task) => void;
@ -121,6 +124,14 @@ export class FluentComponentManager extends Component {
this.viewHandlers = viewHandlers;
}
/**
* Set the TopNavigation reference for managing custom buttons
*/
public setTopNavigation(nav: TopNavigation): void {
this.topNavigation = nav;
console.log("[FluentComponentManager] TopNavigation reference set");
}
/**
* Initialize all view components
*/
@ -447,6 +458,12 @@ export class FluentComponentManager extends Component {
console.log("[FluentComponent] Hiding all components");
this.hideAllComponents();
// Clear custom buttons from TopNavigation before switching views
// They will be re-registered by the new view if needed
if (this.topNavigation) {
this.topNavigation.clearCustomButtons();
}
// Check if current view supports multiple view modes and we're in a non-list mode
const viewModes = this.getAvailableModesForView(viewId, project);
@ -588,6 +605,12 @@ export class FluentComponentManager extends Component {
targetComponent.containerEl.show();
this.currentVisibleComponent = targetComponent;
// Set TopNavigation reference for ContentComponent to enable custom buttons
if (targetComponent === this.contentComponent && this.topNavigation) {
console.log("[FluentComponent] Setting TopNavigation reference for ContentComponent");
this.contentComponent.setTopNavigation(this.topNavigation);
}
// Set view mode first for ContentComponent
if (typeof targetComponent.setViewMode === "function") {
console.log(
@ -685,6 +708,11 @@ export class FluentComponentManager extends Component {
// Hide current component
this.hideAllComponents();
// Clear custom buttons before switching view modes
if (this.topNavigation) {
this.topNavigation.clearCustomButtons();
}
// Based on the current view mode, show the appropriate component
switch (viewMode) {
case "list":
@ -693,6 +721,12 @@ export class FluentComponentManager extends Component {
if (!this.contentComponent) return;
this.contentComponent.containerEl.show();
// Set TopNavigation reference to enable custom buttons
if (this.topNavigation) {
this.contentComponent.setTopNavigation(this.topNavigation);
}
this.contentComponent.setViewMode(viewId as any);
this.contentComponent.setIsTreeView(viewMode === "tree");

View file

@ -943,6 +943,10 @@ export class FluentTaskView extends ItemView {
this.addChild(this.componentManager);
this.componentManager.initializeViewComponents();
// Set TopNavigation reference in ComponentManager
this.componentManager.setTopNavigation(this.topNavigation);
console.log("[Task Genius] TopNavigation reference passed to ComponentManager");
// Sidebar toggle in header and responsive collapse
console.log("[Task Genius] Creating sidebar toggle");
this.layoutManager.createSidebarToggle();

View file

@ -521,6 +521,18 @@ button.fluent-new-task-btn:hover {
gap: 12px;
}
/* Custom buttons container (for dynamic buttons from views) */
.fluent-nav-custom-buttons {
display: flex;
align-items: center;
gap: 4px;
}
.fluent-nav-custom-button-wrapper {
display: flex;
align-items: center;
}
.fluent-nav-icon-button {
position: relative;
width: 36px;