feat(fluent): add modern/classic interface style option

- Add interfaceStyle setting to FluentViewSettings
- Add interface style dropdown in settings UI
- Implement getInterfaceStyle() method in FluentTaskView
- Apply fluent-modern or fluent-classic class to leaf container
- Import fluent-modern.css styles
This commit is contained in:
Quorafind 2025-11-11 20:34:32 +08:00
parent 0da1f199e1
commit f74966edf3
3 changed files with 122 additions and 79 deletions

View file

@ -446,6 +446,7 @@ export interface FluentViewSettings {
defaultWorkspace?: string;
maxOtherViewsBeforeOverflow?: number; // how many other views to show before overflow menu
};
interfaceStyle?: "modern" | "classic";
}
export interface ExperimentalSettings {

View file

@ -9,15 +9,15 @@ import { FluentViewSettings } from "@/common/setting-definition";
export function renderInterfaceSettingsTab(
settingTab: TaskProgressBarSettingTab,
containerEl: HTMLElement,
containerEl: HTMLElement
) {
// Header
new Setting(containerEl)
.setName(t("User Interface"))
.setDesc(
t(
"Choose your preferred interface style and configure how Task Genius displays in your workspace.",
),
"Choose your preferred interface style and configure how Task Genius displays in your workspace."
)
)
.setHeading();
@ -26,8 +26,8 @@ export function renderInterfaceSettingsTab(
.setName(t("Interface Mode"))
.setDesc(
t(
"Select between the modern Fluent interface or the classic Legacy interface.",
),
"Select between the modern Fluent interface or the classic Legacy interface."
)
)
.setHeading();
@ -48,7 +48,7 @@ export function renderInterfaceSettingsTab(
title: t("Fluent"),
subtitle: t("Modern & Sleek"),
description: t(
"New visual design with elegant animations and modern interactions",
"New visual design with elegant animations and modern interactions"
),
preview: createFluentPreview(),
},
@ -57,7 +57,7 @@ export function renderInterfaceSettingsTab(
title: t("Legacy"),
subtitle: t("Classic & Familiar"),
description: t(
"Keep the familiar interface and interaction style you know",
"Keep the familiar interface and interaction style you know"
),
preview: createLegacyPreview(),
},
@ -85,7 +85,7 @@ export function renderInterfaceSettingsTab(
// Re-render the settings to show/hide Fluent-specific options
renderFluentSpecificSettings();
},
}
);
// Set initial selection
@ -112,13 +112,44 @@ export function renderInterfaceSettingsTab(
.setDesc(t("Configure options specific to the Fluent interface."))
.setHeading();
new Setting(fluentSettingsContainer)
.setName(t("Interface Style"))
.setDesc(t("Select the interface style to use."))
.addDropdown((dropdown) => {
dropdown.addOption("modern", t("Modern"));
dropdown.addOption("classic", t("Classic"));
dropdown
.setValue(
settingTab.plugin.settings.fluentView?.interfaceStyle ||
"modern"
)
.onChange(async (value) => {
if (!settingTab.plugin.settings.fluentView) {
settingTab.plugin.settings.fluentView = {
enableFluent: false,
interfaceStyle: "modern",
useWorkspaceSideLeaves: false,
fluentConfig: {
enableWorkspaces: true,
defaultWorkspace: "default",
maxOtherViewsBeforeOverflow: 5,
},
};
}
settingTab.plugin.settings.fluentView.interfaceStyle =
value as "modern" | "classic";
await settingTab.plugin.saveSettings();
new Notice(t("Saved. Reopen the view to apply."));
});
});
// Use workspace side leaves for Sidebar & Details
new Setting(fluentSettingsContainer)
.setName(t("Use workspace side leaves"))
.setDesc(
t(
"Use left/right workspace side leaves for Sidebar and Details. When enabled, the main fluent view won't render in-view sidebar or details.",
),
"Use left/right workspace side leaves for Sidebar and Details. When enabled, the main fluent view won't render in-view sidebar or details."
)
)
.addToggle((toggle) => {
const current =
@ -151,8 +182,8 @@ export function renderInterfaceSettingsTab(
.setName(t("Max other views before overflow"))
.setDesc(
t(
"Number of 'Other Views' to show before grouping the rest into an overflow menu (ellipsis)",
),
"Number of 'Other Views' to show before grouping the rest into an overflow menu (ellipsis)"
)
)
.addText((text) => {
const current =

View file

@ -5,6 +5,7 @@ import "@/styles/fluent/fluent-main.css";
import "@/styles/fluent/fluent-secondary.css";
import "@/styles/fluent/fluent-content-header.css";
import "@/styles/fluent/fluent-project-popover.css";
import "@/styles/fluent/fluent-modern.css";
import {
TopNavigation,
ViewMode,
@ -138,10 +139,14 @@ export class FluentTaskView extends ItemView {
return !!this.plugin.settings.fluentView?.useWorkspaceSideLeaves;
}
private getInterfaceStyle(): "modern" | "classic" {
return this.plugin.settings.fluentView?.interfaceStyle ?? "modern";
}
updateWorkspaceLeafWidth = debounce(() => {
this.app.workspace.trigger(
"task-genius:leaf-width-updated",
this.leaf.width,
this.leaf.width
);
}, 200);
@ -191,7 +196,7 @@ export class FluentTaskView extends ItemView {
} catch (e) {
console.warn(
"[Task Genius] Failed to sync selectedProject from filter state",
e,
e
);
}
@ -203,7 +208,7 @@ export class FluentTaskView extends ItemView {
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
this.updateView();
},
150,
150
);
onResize(): void {
@ -214,7 +219,7 @@ export class FluentTaskView extends ItemView {
const availableModes =
this.componentManager?.getAvailableModesForView(
viewId,
this.viewState.selectedProject,
this.viewState.selectedProject
) ?? [];
const storedMode = this.viewState.viewModeByViewId?.[viewId];
@ -244,7 +249,7 @@ export class FluentTaskView extends ItemView {
const prefersTree = getInitialViewMode(
this.app,
this.plugin,
viewId,
viewId
);
if (prefersTree && availableModes.includes("tree")) {
return "tree";
@ -264,7 +269,7 @@ export class FluentTaskView extends ItemView {
const availableModes = this.componentManager?.getAvailableModesForView(
viewId,
this.viewState.selectedProject,
this.viewState.selectedProject
);
if (
availableModes &&
@ -290,16 +295,22 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
"[Task Genius] Initializing UI, managers, structure, and events...",
"[Task Genius] Initializing UI, managers, structure, and events..."
);
}
this.contentEl.empty();
this.contentEl.toggleClass(
["task-genius-fluent-view", "task-genius-view"],
true,
true
);
if (this.getInterfaceStyle() === "modern") {
(this.leaf as any).containerEl.addClass("fluent-modern");
} else {
(this.leaf as any).containerEl.addClass("fluent-classic");
}
// Create root container (use exact same class as original)
this.rootContainerEl = this.contentEl.createDiv({
cls: "tg-fluent-container",
@ -321,7 +332,7 @@ export class FluentTaskView extends ItemView {
if (this.DEBUG_MODE) {
console.log(
"[Task Genius] ✅ UI, managers, structure, and events initialized",
"[Task Genius] ✅ UI, managers, structure, and events initialized"
);
}
@ -339,7 +350,7 @@ export class FluentTaskView extends ItemView {
this.viewState.currentWorkspace = savedWorkspaceId;
if (this.DEBUG_MODE) {
console.log(
`[Task Genius] Restored workspace ID: ${savedWorkspaceId}`,
`[Task Genius] Restored workspace ID: ${savedWorkspaceId}`
);
}
}
@ -356,7 +367,7 @@ export class FluentTaskView extends ItemView {
this.liveFilterState = state;
this.app.saveLocalStorage(
"task-genius-view-filter",
state || null,
state || null
);
},
setCurrentFilterState: (state) => {
@ -374,14 +385,14 @@ export class FluentTaskView extends ItemView {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
selectedProject,
selectedProject
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
}
return viewMode;
@ -389,7 +400,7 @@ export class FluentTaskView extends ItemView {
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
normalizedMode
);
this.topNavigation?.setViewMode(normalizedMode);
if (clearSearch) {
@ -409,7 +420,7 @@ export class FluentTaskView extends ItemView {
await this.dataManager.loadTasks(false); // Will trigger onTasksLoaded callback
await this.dataManager.registerDataflowListeners();
console.log(
`[Task Genius] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`,
`[Task Genius] ✅ Loaded ${this.tasks.length} tasks, ${this.filteredTasks.length} after filters`
);
// ====================
@ -434,7 +445,7 @@ export class FluentTaskView extends ItemView {
// ====================
if (this.DEBUG_MODE) {
console.log(
`[Task Genius] Finalizing (isInitializing was ${this.isInitializing})`,
`[Task Genius] Finalizing (isInitializing was ${this.isInitializing})`
);
}
@ -464,7 +475,7 @@ export class FluentTaskView extends ItemView {
searchQuery: this.viewState.searchQuery || "",
filterInputValue: this.viewState.filterInputValue || "",
}),
() => this.isInitializing,
() => this.isInitializing
);
this.dataManager.setCallbacks({
onTasksLoaded: (tasks, error) => {
@ -480,7 +491,7 @@ export class FluentTaskView extends ItemView {
this.isLoading = false;
// Apply filters immediately after loading
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -493,7 +504,7 @@ export class FluentTaskView extends ItemView {
},
onUpdateNeeded: (source) => {
console.log(
`[Task Genius] Update needed from source: ${source}`,
`[Task Genius] Update needed from source: ${source}`
);
// Re-apply filters and update view
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
@ -507,7 +518,7 @@ export class FluentTaskView extends ItemView {
this.app,
this.plugin,
() => this.workspaceId,
() => this.useSideLeaves(),
() => this.useSideLeaves()
);
this.actionHandlers.setCallbacks({
onTaskSelectionChanged: (task) => {
@ -523,7 +534,7 @@ export class FluentTaskView extends ItemView {
this.tasks[index] = updatedTask;
// Re-apply filters
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -554,7 +565,7 @@ export class FluentTaskView extends ItemView {
onNavigateToView: (viewId) => {
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
this.viewState.viewMode
);
this.currentViewId = viewId;
@ -562,7 +573,7 @@ export class FluentTaskView extends ItemView {
// This enables the full project overview mode
if (viewId === "projects") {
console.log(
"[Task Genius] Navigating to projects overview - clearing project selection",
"[Task Genius] Navigating to projects overview - clearing project selection"
);
this.viewState.selectedProject = undefined;
@ -576,31 +587,31 @@ export class FluentTaskView extends ItemView {
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
(f: any) => f.property !== "project"
),
}))
.filter(
(g: any) =>
g.filters && g.filters.length > 0,
g.filters && g.filters.length > 0
);
this.liveFilterState = nextState as any;
this.currentFilterState = nextState as any;
this.app.saveLocalStorage(
"task-genius-view-filter",
nextState,
nextState
);
// Broadcast filter change
this.app.workspace.trigger(
"task-genius:filter-changed",
nextState,
nextState
);
}
} catch (e) {
console.warn(
"[Task Genius] Failed to clear project filter",
e,
e
);
}
}
@ -629,17 +640,17 @@ export class FluentTaskView extends ItemView {
// Switch to projects view
this.recordViewModeForView(
this.currentViewId,
this.viewState.viewMode,
this.viewState.viewMode
);
this.currentViewId = "projects";
this.filteredTasks = this.dataManager.applyFilters(this.tasks);
const projectsViewMode = this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
this.viewState.viewMode = projectsViewMode;
this.recordViewModeForView(
this.currentViewId,
projectsViewMode,
projectsViewMode
);
this.topNavigation?.setViewMode(projectsViewMode);
@ -656,7 +667,7 @@ export class FluentTaskView extends ItemView {
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
(f: any) => f.property !== "project"
),
}))
.filter((g: any) => g.filters && g.filters.length > 0); // Remove empty groups
@ -682,23 +693,23 @@ export class FluentTaskView extends ItemView {
this.currentFilterState = nextState as any;
this.app.saveLocalStorage(
"task-genius-view-filter",
nextState,
nextState
);
// Broadcast so any open filter UI reacts and header button shows reset
// The filter-changed event listener will handle applyFilters and updateView
this.app.workspace.trigger(
"task-genius:filter-changed",
nextState,
nextState
);
} catch (e) {
console.warn(
"[Task Genius] Failed to project-sync filter UI state",
e,
e
);
// If filter sync fails, still update the view
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
}
@ -737,7 +748,7 @@ export class FluentTaskView extends ItemView {
viewMode: this.viewState.viewMode,
}),
() => this.currentFilterState,
() => this.liveFilterState,
() => this.liveFilterState
);
this.addChild(this.workspaceStateManager);
@ -745,7 +756,7 @@ export class FluentTaskView extends ItemView {
this.selectionManager = new TaskSelectionManager(
this.app,
this.plugin,
this,
this
);
this.addChild(this.selectionManager);
@ -813,7 +824,7 @@ export class FluentTaskView extends ItemView {
this.rootContainerEl,
this.headerEl, // Obsidian's view header
this.titleEl, // Obsidian's view title element
() => this.filteredTasks.length,
() => this.filteredTasks.length
);
this.layoutManager.setOnSidebarNavigate((viewId) => {
this.actionHandlers.handleNavigate(viewId);
@ -831,7 +842,7 @@ export class FluentTaskView extends ItemView {
onTaskUpdate: async (originalTask, updatedTask) => {
await this.actionHandlers.handleTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
},
});
@ -851,7 +862,7 @@ export class FluentTaskView extends ItemView {
} else {
sidebarEl.hide();
console.log(
"[Task Genius] Using workspace side leaves: skip in-view sidebar",
"[Task Genius] Using workspace side leaves: skip in-view sidebar"
);
}
@ -869,7 +880,7 @@ export class FluentTaskView extends ItemView {
// Sort click callback (future extension point)
},
() => this.actionHandlers.handleSettingsClick(),
[], // Start with empty modes, will be set by updateView
[] // Start with empty modes, will be set by updateView
);
this.addChild(this.topNavigation);
@ -890,7 +901,7 @@ export class FluentTaskView extends ItemView {
onTaskUpdate: async (originalTask, updatedTask) => {
await this.actionHandlers.handleTaskUpdate(
originalTask,
updatedTask,
updatedTask
);
},
onTaskContextMenu: (event, task) => {
@ -898,36 +909,36 @@ export class FluentTaskView extends ItemView {
},
onKanbanTaskStatusUpdate: (taskId, newStatusMark) => {
console.log(
`[FluentTaskView] Kanban status update callback received: taskId=${taskId}, mark=${newStatusMark}`,
`[FluentTaskView] Kanban status update callback received: taskId=${taskId}, mark=${newStatusMark}`
);
console.log(
`[FluentTaskView] Current task list size: ${this.tasks.length}`,
`[FluentTaskView] Current task list size: ${this.tasks.length}`
);
const task = this.tasks.find((t) => t.id === taskId);
if (task) {
console.log(
"[FluentTaskView] Task found, delegating to action handler",
"[FluentTaskView] Task found, delegating to action handler"
);
this.actionHandlers.handleKanbanTaskStatusUpdate(
task,
newStatusMark,
newStatusMark
);
} else {
console.error(
`[FluentTaskView] CRITICAL: Task ${taskId} not found in local task list`,
`[FluentTaskView] CRITICAL: Task ${taskId} not found in local task list`
);
console.error(
"[FluentTaskView] Available task IDs:",
this.tasks.slice(0, 5).map((t) => t.id),
this.tasks.slice(0, 5).map((t) => t.id)
);
console.error(
"[FluentTaskView] This indicates a synchronization issue between kanban and main view",
"[FluentTaskView] This indicates a synchronization issue between kanban and main view"
);
}
},
},
this.selectionManager,
this.selectionManager
);
this.addChild(this.componentManager);
this.componentManager.initializeViewComponents();
@ -976,7 +987,7 @@ export class FluentTaskView extends ItemView {
this.workspaceStateManager.captureFilterStateSnapshot();
if (snapshot) {
await this.workspaceStateManager.saveFilterStateImmediately(
snapshot,
snapshot
);
}
@ -998,7 +1009,7 @@ export class FluentTaskView extends ItemView {
this.liveFilterState = state;
this.app.saveLocalStorage(
"task-genius-view-filter",
state || null,
state || null
);
},
setCurrentFilterState: (state) => {
@ -1017,14 +1028,14 @@ export class FluentTaskView extends ItemView {
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
selectedProject,
selectedProject
) ?? [];
if (
availableModes.length > 0 &&
!availableModes.includes(viewMode)
) {
return this.ensureViewModeForView(
this.currentViewId,
this.currentViewId
);
}
return viewMode;
@ -1032,7 +1043,7 @@ export class FluentTaskView extends ItemView {
this.viewState.viewMode = normalizedMode;
this.recordViewModeForView(
this.currentViewId,
normalizedMode,
normalizedMode
);
this.topNavigation?.setViewMode(normalizedMode);
if (clearSearch) {
@ -1048,7 +1059,7 @@ export class FluentTaskView extends ItemView {
// Reload tasks
await this.dataManager.loadTasks();
}
}),
})
);
// Workspace overrides saved event
@ -1058,7 +1069,7 @@ export class FluentTaskView extends ItemView {
await this.workspaceStateManager.applyWorkspaceSettings();
await this.dataManager.loadTasks();
}
}),
})
);
// Settings changed event (skip for filter state changes)
@ -1066,7 +1077,7 @@ export class FluentTaskView extends ItemView {
on(this.app, Events.SETTINGS_CHANGED, async () => {
// Reload on settings change (unless caused by filter state save)
await this.dataManager.loadTasks();
}),
})
);
}
@ -1076,8 +1087,8 @@ export class FluentTaskView extends ItemView {
"task-genius:filter-changed",
(filterState: RootFilterState, leafId?: string) => {
this.handleFilterChanged(filterState, leafId);
},
),
}
)
);
// Sidebar selection changed (when using side leaves)
@ -1091,7 +1102,7 @@ export class FluentTaskView extends ItemView {
) {
this.currentViewId = payload.selectionId;
this.filteredTasks = this.dataManager.applyFilters(
this.tasks,
this.tasks
);
this.updateView();
this.workspaceStateManager.saveFilterStateToWorkspace();
@ -1102,11 +1113,11 @@ export class FluentTaskView extends ItemView {
) {
// Use the full project selection logic via actionHandlers
this.actionHandlers.handleProjectSelect(
payload.selectionId || "",
payload.selectionId || ""
);
}
}
}),
})
);
}
@ -1134,14 +1145,14 @@ export class FluentTaskView extends ItemView {
}
console.log(
`[Task Genius] Proceeding with view update for ${this.currentViewId}`,
`[Task Genius] Proceeding with view update for ${this.currentViewId}`
);
// Update top navigation available modes based on current view
const availableModes =
this.componentManager?.getAvailableModesForView(
this.currentViewId,
this.viewState.selectedProject,
this.viewState.selectedProject
) ?? [];
this.topNavigation?.updateAvailableModes(availableModes);
@ -1197,7 +1208,7 @@ export class FluentTaskView extends ItemView {
this.filteredTasks,
this.currentFilterState,
this.viewState.viewMode,
this.viewState.selectedProject,
this.viewState.selectedProject
);
}