feat(core): integrate FileSource and add URI handler support

- Add ObsidianUriHandler for handling obsidian:// URIs
- Update plugin initialization to support FileSource
- Integrate file filter manager in task indexer
- Add URI navigation support in settings
- Improve settings search with URI parameters
This commit is contained in:
Quorafind 2025-08-23 11:34:00 +08:00
parent d58f4873ce
commit a7e4dafb2a
4 changed files with 472 additions and 4 deletions

View file

@ -112,7 +112,8 @@ export class TaskIndexer extends Component implements TaskIndexerInterface {
this.vault.on("modify", (file) => {
if (
file instanceof TFile &&
isSupportedFileWithFilter(file, this.fileFilterManager)
// Inline parsing scope for indexer (inline tasks)
isSupportedFileWithFilter(file, this.fileFilterManager, "inline")
) {
this.queueFileForIndexing(file);
}
@ -124,7 +125,8 @@ export class TaskIndexer extends Component implements TaskIndexerInterface {
this.vault.on("delete", (file) => {
if (
file instanceof TFile &&
isSupportedFileWithFilter(file, this.fileFilterManager)
// Inline parsing scope for indexer (inline tasks)
isSupportedFileWithFilter(file, this.fileFilterManager, "inline")
) {
this.removeFileFromIndex(file);
}
@ -136,7 +138,8 @@ export class TaskIndexer extends Component implements TaskIndexerInterface {
this.vault.on("create", (file) => {
if (
file instanceof TFile &&
isSupportedFileWithFilter(file, this.fileFilterManager)
// Inline parsing scope for indexer (inline tasks)
isSupportedFileWithFilter(file, this.fileFilterManager, "inline")
) {
this.queueFileForIndexing(file);
}

View file

@ -98,6 +98,7 @@ import { autoDateManagerExtension } from "./editor-extensions/date-time/date-man
import { taskMarkCleanupExtension } from "./editor-extensions/task-operations/mark-cleanup";
import { ViewManager } from "./pages/ViewManager";
import { IcsManager } from "./managers/ics-manager";
import { ObsidianUriHandler } from "./utils/ObsidianUriHandler";
import { VersionManager } from "./managers/version-manager";
import { RebuildProgressManager } from "./managers/rebuild-progress-manager";
import { OnboardingConfigManager } from "./managers/onboarding-manager";
@ -245,6 +246,9 @@ export default class TaskProgressBarPlugin extends Plugin {
// MCP Server manager instance (desktop only)
mcpServerManager?: McpServerManager;
// URI handler instance
uriHandler?: ObsidianUriHandler;
// OnCompletion manager instance
onCompletionManager?: OnCompletionManager;
@ -270,6 +274,10 @@ export default class TaskProgressBarPlugin extends Plugin {
// Initialize global suggest manager
this.globalSuggestManager = new SuggestManager(this.app, this);
// Initialize URI handler
this.uriHandler = new ObsidianUriHandler(this);
this.uriHandler.register();
// Initialize rebuild progress manager
this.rebuildProgressManager = new RebuildProgressManager();
// Initialize task management systems

View file

@ -44,6 +44,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Tabs management
private currentTab: string = "general";
public containerEl: HTMLElement;
private tabs: Array<{
id: string;
name: string;
@ -202,7 +203,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
this.applyDebounceTimer = window.setTimeout(async () => {
await plugin.saveSettings();
// Parsing configuration is now handled by dataflow
// Update dataflow orchestrator with new settings
if (plugin.dataflowOrchestrator) {
plugin.dataflowOrchestrator.updateSettings(plugin.settings);
}
// Trigger view updates to reflect setting changes
await plugin.triggerViewUpdate();
@ -394,6 +398,57 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
this.display();
}
/**
* Navigate to a specific tab via URI
*/
public navigateToTab(tabId: string, section?: string, search?: string): void {
// Set the current tab
this.currentTab = tabId;
// Re-display the settings
this.display();
// Wait for display to complete
setTimeout(() => {
// If search is provided, perform search
if (search && this.searchComponent) {
this.searchComponent.performSearch(search);
}
// If section is provided, scroll to it
if (section) {
this.scrollToSection(section);
}
}, 100);
}
/**
* Scroll to a specific section within the current tab
*/
private scrollToSection(sectionId: string): void {
// Look for headers containing the section ID
const headers = this.containerEl.querySelectorAll("h3, h4");
headers.forEach((header: HTMLElement) => {
const headerText = header.textContent?.toLowerCase();
if (headerText && headerText.includes(sectionId.replace("-", " "))) {
header.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
// Special handling for MCP sections
if (sectionId === "cursor" && this.currentTab === "mcp-integration") {
const cursorSection = this.containerEl.querySelector(".mcp-client-section");
if (cursorSection) {
const header = cursorSection.querySelector(".mcp-client-header");
if (header && header.textContent?.includes("Cursor")) {
// Click to expand
(header as HTMLElement).click();
cursorSection.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}
}
private createTabSection(tabId: string): HTMLElement {
// Get the sections container
const sectionsContainer = this.containerEl.querySelector(

View file

@ -0,0 +1,402 @@
/**
* Obsidian URI Handler for Task Genius
* Handles custom URI scheme: obsidian://task-genius/...
*/
import { Modal, Notice } from "obsidian";
import TaskProgressBarPlugin from "../index";
import { t } from "../translations/helper";
export interface UriParams {
action?: string;
tab?: string;
section?: string;
search?: string;
[key: string]: string | undefined;
}
export class ObsidianUriHandler {
private plugin: TaskProgressBarPlugin;
constructor(plugin: TaskProgressBarPlugin) {
this.plugin = plugin;
}
/**
* Register the URI handler with Obsidian
*/
register(): void {
this.plugin.registerObsidianProtocolHandler(
"task-genius",
async (params: UriParams) => {
await this.handleUri(params);
}
);
}
/**
* Handle incoming URI requests
*/
private async handleUri(params: UriParams): Promise<void> {
const { action, tab, section, search } = params;
// Default to settings action if not specified
const uriAction = action || "settings";
switch (uriAction) {
case "settings":
await this.handleSettingsUri(params);
break;
case "create-task":
await this.handleCreateTaskUri(params);
break;
case "open-view":
await this.handleOpenViewUri(params);
break;
default:
new Notice(t("Unknown URI action: ") + uriAction);
}
}
/**
* Handle settings-related URI
* Example: obsidian://task-genius/settings?tab=mcp-integration&action=enable
*/
private async handleSettingsUri(params: UriParams): Promise<void> {
const { tab, section, search } = params;
// Open settings
const settings = (this.plugin.app as any).setting;
settings.open();
settings.openTabById(this.plugin.manifest.id);
// Wait for settings to be ready
await new Promise(resolve => setTimeout(resolve, 100));
// Navigate to specific tab if provided
if (tab) {
this.navigateToSettingsTab(tab, section, search);
}
// Handle specific actions
if (params.action) {
await this.handleSettingsAction(params.action, tab);
}
}
/**
* Navigate to a specific settings tab
*/
private navigateToSettingsTab(
tabName: string,
section?: string,
search?: string
): void {
// Use the settingTab's navigation method if available
if (this.plugin.settingTab && this.plugin.settingTab.navigateToTab) {
this.plugin.settingTab.navigateToTab(tabName, section, search);
return;
}
// Fallback: Map tab names to tab indices or identifiers
const tabMap: Record<string, string> = {
"general": "general",
"index": "index",
"view-settings": "view-settings",
"file-filter": "file-filter",
"progress-bar": "progress-bar",
"task-status": "task-status",
"task-handler": "task-handler",
"workflow": "workflow",
"reward": "reward",
"habit": "habit",
"mcp-integration": "mcp-integration",
"ics": "ics",
"time-parsing": "time-parsing",
"beta-test": "beta-test",
"about": "about"
};
const tabId = tabMap[tabName];
if (!tabId) {
new Notice(t("Unknown settings tab: ") + tabName);
return;
}
// Fallback implementation
const modal = (this.plugin.app as any).setting.activeTab;
if (!modal) return;
// Find and click the tab
const tabButtons = modal.containerEl.querySelectorAll(".settings-tab");
tabButtons.forEach((button: HTMLElement) => {
const buttonText = button.textContent?.toLowerCase();
if (buttonText && buttonText.includes(tabName.replace("-", " "))) {
button.click();
// If there's a section, try to scroll to it
if (section) {
setTimeout(() => {
this.scrollToSection(section);
}, 200);
}
// If there's a search term, try to focus the search
if (search) {
setTimeout(() => {
this.performSettingsSearch(search);
}, 300);
}
}
});
}
/**
* Scroll to a specific section within the settings
*/
private scrollToSection(sectionId: string): void {
const modal = (this.plugin.app as any).setting.activeTab;
if (!modal) return;
// Look for section headers
const headers = modal.containerEl.querySelectorAll("h3, h4");
headers.forEach((header: HTMLElement) => {
const headerText = header.textContent?.toLowerCase();
if (headerText && headerText.includes(sectionId.replace("-", " "))) {
header.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
// Special handling for specific sections
if (sectionId === "cursor") {
// Look for Cursor configuration section
const cursorSection = modal.containerEl.querySelector(".mcp-client-section");
if (cursorSection) {
const header = cursorSection.querySelector(".mcp-client-header");
if (header && header.textContent?.includes("Cursor")) {
// Click to expand
(header as HTMLElement).click();
cursorSection.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}
}
/**
* Perform a search in the settings
*/
private performSettingsSearch(searchTerm: string): void {
const modal = (this.plugin.app as any).setting.activeTab;
if (!modal) return;
// Find the search input
const searchInput = modal.containerEl.querySelector(
"input[type='search'], input.search-input"
) as HTMLInputElement;
if (searchInput) {
searchInput.value = searchTerm;
searchInput.dispatchEvent(new Event("input", { bubbles: true }));
searchInput.focus();
}
}
/**
* Handle specific actions within settings
*/
private async handleSettingsAction(
action: string,
tab?: string
): Promise<void> {
// Wait for settings to be fully loaded
await new Promise(resolve => setTimeout(resolve, 500));
const modal = (this.plugin.app as any).setting.activeTab as Modal;
if (!modal) return;
switch (action) {
case "enable":
if (tab === "mcp-integration") {
// Find and click the enable toggle
const toggle = modal.containerEl.querySelector(
".setting-item:has(.setting-item-name:contains('Enable MCP Server')) .checkbox-container input"
) as HTMLInputElement;
if (toggle && !toggle.checked) {
toggle.click();
}
}
break;
case "test":
if (tab === "mcp-integration") {
// Find and click the test button
const testButton = Array.from(
modal.containerEl.querySelectorAll("button")
).find(btn => btn.textContent === t("Test"));
if (testButton) {
(testButton as HTMLButtonElement).click();
}
}
break;
case "regenerate-token":
if (tab === "mcp-integration") {
// Find and click the regenerate button
const regenerateButton = Array.from(
modal.containerEl.querySelectorAll("button")
).find(btn => btn.textContent === t("Regenerate"));
if (regenerateButton) {
(regenerateButton as HTMLButtonElement).click();
}
}
break;
}
}
/**
* Handle create task URI
* Example: obsidian://task-genius/create-task?content=My%20Task&project=Work
*/
private async handleCreateTaskUri(params: UriParams): Promise<void> {
const {
content,
project,
context,
tags,
priority,
dueDate,
startDate
} = params;
if (!content) {
new Notice(t("Task content is required"));
return;
}
// Parse tags if provided as comma-separated
const taskTags = tags ? tags.split(",").map(t => t.trim()) : [];
// Create the task using WriteAPI
try {
if (!this.plugin.writeAPI) {
new Notice(t("Task system not initialized"));
return;
}
// Get the daily note or create in inbox
const dailyNotePath = this.plugin.app.workspace.getActiveFile()?.path ||
`Daily/${new Date().toISOString().split('T')[0]}.md`;
await this.plugin.writeAPI.createTask({
content: decodeURIComponent(content),
project: project ? decodeURIComponent(project) : undefined,
context: context ? decodeURIComponent(context) : undefined,
tags: taskTags,
priority: priority ? parseInt(priority) : undefined,
dueDate: dueDate || undefined,
startDate: startDate || undefined,
filePath: dailyNotePath
});
new Notice(t("Task created successfully"));
} catch (error) {
console.error("Failed to create task from URI:", error);
new Notice(t("Failed to create task"));
}
}
/**
* Handle open view URI
* Example: obsidian://task-genius/open-view?type=inbox
*/
private async handleOpenViewUri(params: UriParams): Promise<void> {
const { type } = params;
if (!type) {
new Notice(t("View type is required"));
return;
}
// Map view types to leaf types
const viewMap: Record<string, string> = {
"inbox": "task-progress-bar-view",
"forecast": "task-progress-bar-view",
"project": "task-progress-bar-view",
"tag": "task-progress-bar-view",
"review": "task-progress-bar-view",
"calendar": "task-progress-bar-view",
"gantt": "task-progress-bar-view",
"kanban": "task-progress-bar-view",
"matrix": "task-progress-bar-view",
"table": "task-progress-bar-view"
};
const leafType = viewMap[type];
if (!leafType) {
new Notice(t("Unknown view type: ") + type);
return;
}
// Open the view
const leaf = this.plugin.app.workspace.getLeaf(true);
await leaf.setViewState({
type: leafType,
state: { viewType: type }
});
this.plugin.app.workspace.revealLeaf(leaf);
}
/**
* Generate URI for settings
*/
static generateSettingsUri(
tab?: string,
section?: string,
action?: string,
search?: string
): string {
const params = new URLSearchParams();
if (tab) params.set("tab", tab);
if (section) params.set("section", section);
if (action) params.set("action", action);
if (search) params.set("search", search);
const queryString = params.toString();
return `obsidian://task-genius/settings${queryString ? "?" + queryString : ""}`;
}
/**
* Generate URI for creating a task
*/
static generateCreateTaskUri(
content: string,
options?: {
project?: string;
context?: string;
tags?: string[];
priority?: number;
dueDate?: string;
startDate?: string;
}
): string {
const params = new URLSearchParams();
params.set("content", encodeURIComponent(content));
if (options?.project) params.set("project", encodeURIComponent(options.project));
if (options?.context) params.set("context", encodeURIComponent(options.context));
if (options?.tags) params.set("tags", options.tags.join(","));
if (options?.priority) params.set("priority", options.priority.toString());
if (options?.dueDate) params.set("dueDate", options.dueDate);
if (options?.startDate) params.set("startDate", options.startDate);
return `obsidian://task-genius/create-task?${params.toString()}`;
}
/**
* Generate URI for opening a view
*/
static generateOpenViewUri(viewType: string): string {
return `obsidian://task-genius/open-view?type=${viewType}`;
}
}