feat(settings): enhance index settings with tabbed source configuration

- Add tabbed interface for Checkbox Tasks and File Tasks configuration
- Introduce FileFilterScopeControls to manage inline vs file task sources
- Create ViewComponent base class for unified view component behaviors
- Improve settings UI with clearer separation of task source types
- Add scope controls to FileFilterManager for granular source management
- Update calendar component formatting and add configOverride support
- Enhance CSS styling for tabbed settings interface
This commit is contained in:
Quorafind 2025-10-09 21:23:51 +08:00
parent af31b39f30
commit 0ae3957144
13 changed files with 1010 additions and 519 deletions

View file

@ -649,10 +649,16 @@ export enum FilterMode {
BLACKLIST = "blacklist",
}
export interface FileFilterScopeControls {
inlineTasksEnabled: boolean;
fileTasksEnabled: boolean;
}
export interface FileFilterSettings {
enabled: boolean;
mode: FilterMode;
rules: FileFilterRule[];
scopeControls?: FileFilterScopeControls;
}
/** MCP Server Configuration */
@ -1522,6 +1528,10 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
rules: [
// No default rules - let users explicitly choose via preset templates
],
scopeControls: {
inlineTasksEnabled: true,
fileTasksEnabled: true,
},
},
// OnCompletion Defaults

View file

@ -65,8 +65,8 @@ export class CalendarComponent extends Component {
private badgeEventsCacheVersion: number = 0;
// Per-view override from Bases (firstDayOfWeek, hideWeekends, ...)
private configOverride: Partial<import("@/common/setting-definition").CalendarSpecificConfig> | null = null;
// Per-view override from Bases (firstDayOfWeek, hideWeekends, ...)
private configOverride: Partial<import("@/common/setting-definition").CalendarSpecificConfig> | null = null;
constructor(
app: App,
@ -162,8 +162,6 @@ export class CalendarComponent extends Component {
this.app.saveLocalStorage(
"task-genius:calendar-view",
this.currentViewMode
);
}
}
@ -703,7 +701,8 @@ export class CalendarComponent extends Component {
let endDate: Date;
switch (this.currentViewMode) {
case "month":
case "month": {
// For month view, compute for the entire grid (including previous/next month days)
const startOfMonth = this.currentDate.clone().startOf("month");
const endOfMonth = this.currentDate.clone().endOf("month");
@ -715,7 +714,7 @@ export class CalendarComponent extends Component {
const gridStart = startOfMonth
.clone()
.weekday(firstDayOfWeek - 7);
let gridEnd = endOfMonth.clone().weekday(firstDayOfWeek + 6);
const gridEnd = endOfMonth.clone().weekday(firstDayOfWeek + 6);
// Ensure at least 42 days (6 weeks)
if (gridEnd.diff(gridStart, "days") + 1 < 42) {
@ -727,13 +726,16 @@ export class CalendarComponent extends Component {
startDate = gridStart.toDate();
endDate = gridEnd.toDate();
break;
}
case "week": {
case "week":
const startOfWeek = this.currentDate.clone().startOf("week");
const endOfWeek = this.currentDate.clone().endOf("week");
startDate = startOfWeek.toDate();
endDate = endOfWeek.toDate();
break;
}
case "day":
startDate = this.currentDate.clone().startOf("day").toDate();
@ -887,7 +889,7 @@ export class CalendarComponent extends Component {
new QuickCaptureModal(
this.app,
this.plugin,
{ dueDate: moment(day).toDate() },
{dueDate: moment(day).toDate()},
true
).open();
} else if (options.behavior === "open-task-view") {
@ -934,19 +936,19 @@ export class CalendarComponent extends Component {
this.params?.onTaskCompleted?.(event);
};
// Allow external overrides (e.g., from Bases) and compute effective config
public setConfigOverride(
override: Partial<import("@/common/setting-definition").CalendarSpecificConfig> | null
): void {
this.configOverride = override ?? null;
// Re-render to apply new config
this.render();
}
// Allow external overrides (e.g., from Bases) and compute effective config
public setConfigOverride(
override: Partial<import("@/common/setting-definition").CalendarSpecificConfig> | null
): void {
this.configOverride = override ?? null;
// Re-render to apply new config
this.render();
}
private getEffectiveCalendarConfig(): Partial<import("@/common/setting-definition").CalendarSpecificConfig> {
const baseCfg = this.plugin.settings.viewConfiguration.find((v) => v.id === this.viewId)?.specificConfig as Partial<import("@/common/setting-definition").CalendarSpecificConfig> | undefined;
return { ...(baseCfg ?? {}), ...(this.configOverride ?? {}) };
}
private getEffectiveCalendarConfig(): Partial<import("@/common/setting-definition").CalendarSpecificConfig> {
const baseCfg = this.plugin.settings.viewConfiguration.find((v) => v.id === this.viewId)?.specificConfig as Partial<import("@/common/setting-definition").CalendarSpecificConfig> | undefined;
return {...(baseCfg ?? {}), ...(this.configOverride ?? {})};
}
}

View file

@ -57,6 +57,8 @@ export abstract class BaseQuickCaptureModal extends Modal {
protected taskMetadata: TaskMetadata = {};
protected currentMode: QuickCaptureMode;
protected suggestManager: SuggestManager;
protected inlineModeAvailable: boolean = true;
protected fileModeAvailable: boolean = false;
// UI Elements
protected headerContainer: HTMLElement | null = null;
@ -78,6 +80,22 @@ export abstract class BaseQuickCaptureModal extends Modal {
this.plugin = plugin;
this.currentMode = initialMode;
const scopeControls =
this.plugin.settings.fileFilter?.scopeControls;
this.inlineModeAvailable =
scopeControls?.inlineTasksEnabled !== false;
this.fileModeAvailable =
(this.plugin.settings.fileSource?.enabled ?? false) &&
scopeControls?.fileTasksEnabled !== false;
if (!scopeControls) {
this.inlineModeAvailable = true;
this.fileModeAvailable =
this.plugin.settings.fileSource?.enabled ?? false;
}
if (!this.inlineModeAvailable && !this.fileModeAvailable) {
this.inlineModeAvailable = true;
}
// Initialize suggest manager
this.suggestManager = new SuggestManager(app, plugin);
@ -91,11 +109,14 @@ export abstract class BaseQuickCaptureModal extends Modal {
}
// If FileSource is disabled, force mode to checkbox
if (
this.currentMode === "file" &&
!this.plugin.settings?.fileSource?.enabled
) {
if (this.currentMode === "file" && !this.fileModeAvailable) {
this.currentMode = "checkbox";
} else if (
this.currentMode === "checkbox" &&
!this.inlineModeAvailable &&
this.fileModeAvailable
) {
this.currentMode = "file";
}
// Initialize settings
@ -187,38 +208,43 @@ export abstract class BaseQuickCaptureModal extends Modal {
});
// Checkbox mode button (save as checkbox task)
const checkboxButton = new ButtonComponent(tabContainer)
.setClass("quick-capture-tab")
.onClick(() => this.switchMode("checkbox"));
if (this.inlineModeAvailable) {
const checkboxButton = new ButtonComponent(tabContainer)
.setClass("quick-capture-tab")
.onClick(() => this.switchMode("checkbox"));
checkboxButton.buttonEl.toggleClass("clickable-icon", true);
checkboxButton.buttonEl.toggleClass("clickable-icon", true);
const checkboxButtonEl = checkboxButton.buttonEl;
checkboxButtonEl.setAttribute("role", "tab");
checkboxButtonEl.setAttribute(
"aria-selected",
String(this.currentMode === "checkbox")
);
checkboxButtonEl.setAttribute("aria-controls", "quick-capture-content");
checkboxButtonEl.setAttribute("data-mode", "checkbox");
const checkboxButtonEl = checkboxButton.buttonEl;
checkboxButtonEl.setAttribute("role", "tab");
checkboxButtonEl.setAttribute(
"aria-selected",
String(this.currentMode === "checkbox")
);
checkboxButtonEl.setAttribute(
"aria-controls",
"quick-capture-content"
);
checkboxButtonEl.setAttribute("data-mode", "checkbox");
if (this.currentMode === "checkbox") {
checkboxButton.setClass("active");
if (this.currentMode === "checkbox") {
checkboxButton.setClass("active");
}
// Manually create spans for icon and text
checkboxButtonEl.empty();
const checkboxIconSpan = checkboxButtonEl.createSpan({
cls: "quick-capture-tab-icon",
});
setIcon(checkboxIconSpan, "check-square");
checkboxButtonEl.createSpan({
text: t("Checkbox"),
cls: "quick-capture-tab-text",
});
}
// Manually create spans for icon and text
checkboxButtonEl.empty();
const checkboxIconSpan = checkboxButtonEl.createSpan({
cls: "quick-capture-tab-icon",
});
setIcon(checkboxIconSpan, "check-square");
checkboxButtonEl.createSpan({
text: t("Checkbox"),
cls: "quick-capture-tab-text",
});
// File mode button (save as file) - only when FileSource is enabled
if (this.plugin.settings?.fileSource?.enabled) {
if (this.fileModeAvailable) {
const fileButton = new ButtonComponent(tabContainer)
.setClass("quick-capture-tab")
.onClick(() => this.switchMode("file"));
@ -249,6 +275,10 @@ export abstract class BaseQuickCaptureModal extends Modal {
});
}
if (!(this.fileModeAvailable && this.inlineModeAvailable)) {
tabContainer.classList.add("is-hidden");
}
// Right side: Clear button with improved styling
const clearButton = this.headerContainer.createEl("button", {
text: t("Clear"),
@ -305,8 +335,8 @@ export abstract class BaseQuickCaptureModal extends Modal {
*/
protected async switchMode(mode: QuickCaptureMode): Promise<void> {
if (mode === this.currentMode) return;
// Prevent switching to file mode when FileSource is disabled
if (mode === "file" && !this.plugin.settings?.fileSource?.enabled) {
// Prevent switching to unsupported modes
if (mode === "file" && !this.fileModeAvailable) {
new Notice(
t(
"File Task is disabled. Enable FileSource in Settings to use File mode."
@ -314,6 +344,9 @@ export abstract class BaseQuickCaptureModal extends Modal {
);
return;
}
if (mode === "checkbox" && !this.inlineModeAvailable) {
return;
}
// Save current state
const savedContent = this.capturedContent;
@ -334,14 +367,13 @@ export abstract class BaseQuickCaptureModal extends Modal {
// Update tab active states
const tabs =
this.headerContainer?.querySelectorAll(".quick-capture-tab");
tabs?.forEach((tab, index) => {
tabs?.forEach((tab) => {
tab.removeClass("active");
if (
(index === 0 && mode === "checkbox") ||
(index === 1 && mode === "file")
) {
const tabMode = tab.getAttribute("data-mode");
if (tabMode === mode) {
tab.addClass("active");
}
tab.setAttribute("aria-selected", String(tabMode === mode));
});
// Update only the target display instead of recreating everything

View file

@ -14,14 +14,28 @@ import { ListConfigModal } from "@/components/ui/modals/ListConfigModal";
/**
* Create File Task settings UI
*/
export interface FileSourceSettingsOptions {
showEnableToggle?: boolean;
}
export function createFileSourceSettings(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin
plugin: TaskProgressBarPlugin,
options: FileSourceSettingsOptions = {}
): void {
const config = plugin.settings?.fileSource;
if (!config) {
console.warn(
"[FileSourceSettings] Missing fileSource configuration on plugin settings"
);
return;
}
// Main FileSource enable/disable toggle
createEnableToggle(containerEl, plugin, config);
if (options.showEnableToggle !== false) {
createEnableToggle(containerEl, plugin, config);
}
if (config.enabled) {
// Recognition strategies section

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
import { App, Component } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import { ViewMode } from "@/common/setting-definition";
/**
* Base class for view components to unify common behaviors across special views
* (kanban, calendar, gantt, forecast, quadrant, table, twocolumn, ...)
*
* Note: Intentionally minimal and non-invasive so existing components can
* progressively migrate to extend this class without large refactors.
*/
export abstract class ViewComponent<TOverride = any> extends Component {
protected app: App;
protected plugin: TaskProgressBarPlugin;
protected currentViewId: string;
protected configOverride: Partial<TOverride> | null = null;
constructor(app: App, plugin: TaskProgressBarPlugin, viewId: string) {
super();
this.app = app;
this.plugin = plugin;
this.currentViewId = viewId;
}
/**
* Inject per-view override configuration (e.g., from Bases view config)
* Subclasses should merge this with their own config and refresh UI if needed.
*/
public setConfigOverride(override: Partial<TOverride> | null): void {
this.configOverride = override ?? null;
}
/** Optional hooks for specific view components to implement */
// Provide tasks to the component (filteredTasks, allTasks, forceRefresh?)
public setTasks(_tasks: Task[], _allTasks?: Task[], _forceRefresh?: boolean): void {}
// Update tasks incrementally (some components expose this instead of setTasks)
public updateTasks(_tasks: Task[]): void {}
// Switch internal mode (some views support different modes within the same component)
public setViewMode(_viewMode: ViewMode, _project?: string | null): void {}
}

View file

@ -5,7 +5,7 @@ import "@/styles/modal.css";
export interface ListConfigModalParams {
title: string;
description: string;
description: string | DocumentFragment;
placeholder?: string;
values: string[];
onSave: (values: string[]) => void;

View file

@ -6,7 +6,10 @@
*/
import { normalizePath, TFile, TFolder } from "obsidian";
import { FilterMode } from "../common/setting-definition";
import {
FilterMode,
type FileFilterScopeControls,
} from "../common/setting-definition";
/**
* Filter rule types
@ -25,6 +28,7 @@ export interface FileFilterConfig {
enabled: boolean;
mode: FilterMode;
rules: FilterRule[];
scopeControls?: FileFilterScopeControls;
}
/**
@ -109,6 +113,10 @@ export class FileFilterManager {
private fileSet: Set<string> = new Set(); // global (legacy)
private patternRegexes: RegExp[] = []; // global (legacy)
private cache: Map<string, boolean> = new Map();
private scopeControls: FileFilterScopeControls = {
inlineTasksEnabled: true,
fileTasksEnabled: true,
};
// Scoped indexes for per-rule scope control
private folderTrieInline: PathTrie = new PathTrie();
@ -120,6 +128,9 @@ export class FileFilterManager {
constructor(config: FileFilterConfig) {
this.config = config;
this.scopeControls = this.normalizeScopeControls(
config.scopeControls
);
this.rebuildIndexes();
}
@ -128,10 +139,37 @@ export class FileFilterManager {
*/
updateConfig(config: FileFilterConfig): void {
this.config = config;
this.scopeControls = this.normalizeScopeControls(
config.scopeControls
);
this.rebuildIndexes();
this.clearCache();
}
private normalizeScopeControls(
scopeControls?: FileFilterScopeControls
): FileFilterScopeControls {
return {
inlineTasksEnabled:
scopeControls?.inlineTasksEnabled !== false,
fileTasksEnabled:
scopeControls?.fileTasksEnabled !== false,
};
}
private isScopeEnabled(scope: "both" | "inline" | "file"): boolean {
if (scope === "inline") {
return this.scopeControls.inlineTasksEnabled !== false;
}
if (scope === "file") {
return this.scopeControls.fileTasksEnabled !== false;
}
return (
this.scopeControls.inlineTasksEnabled !== false ||
this.scopeControls.fileTasksEnabled !== false
);
}
/**
* Check if a file should be included in indexing
*/
@ -139,6 +177,9 @@ export class FileFilterManager {
file: TFile,
scope: "both" | "inline" | "file" = "both"
): boolean {
if (!this.isScopeEnabled(scope)) {
return false;
}
if (!this.config.enabled) {
return true;
}
@ -162,6 +203,9 @@ export class FileFilterManager {
folder: TFolder,
scope: "both" | "inline" | "file" = "both"
): boolean {
if (!this.isScopeEnabled(scope)) {
return false;
}
if (!this.config.enabled) {
return true;
}
@ -185,6 +229,9 @@ export class FileFilterManager {
path: string,
scope: "both" | "inline" | "file" = "both"
): boolean {
if (!this.isScopeEnabled(scope)) {
return false;
}
if (!this.config.enabled) {
return true;
}

View file

@ -16,9 +16,9 @@
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-xl: 0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* Modal specific styles */
@ -51,8 +51,7 @@
/* Header section */
.onboarding-view .onboarding-header {
padding: calc(var(--onboarding-spacing) * 4) var(--onboarding-spacing)
var(--size-4-2) var(--onboarding-spacing);
padding: calc(var(--onboarding-spacing) * 4) var(--onboarding-spacing) var(--size-4-2) var(--onboarding-spacing);
text-align: center;
}
@ -266,7 +265,7 @@
var(--background-primary) 100%
);
box-shadow: var(--shadow-md),
0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
}
.onboarding-view .settings-check-action-card:hover {
@ -1524,8 +1523,8 @@
filter: blur(4px);
transform: translateY(-2px);
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
@ -1759,7 +1758,7 @@
.selectable-card {
border: 1px solid var(--background-modifier-border);
border-radius: var(--onboarding-border-radius);
border-radius: var(--onboarding-border-radius, var(--radius-m));
padding: var(--size-4-4);
cursor: pointer;
transition: var(--onboarding-transition);

View file

@ -85,6 +85,10 @@
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.quick-capture-tabs.is-hidden {
display: none;
}
/* Quick Capture Tab Button Icon and Text - shadcn style */
.quick-capture-tab-icon {
margin-right: 6px;

View file

@ -221,3 +221,56 @@
border-top: 1px solid var(--background-modifier-border);
padding-top: var(--size-4-4);
}
.fluent-view-tabs.tg-index-task-source-switcher {
display: flex;
align-items: stretch;
justify-content: center;
gap: var(--size-2-2);
background-color: var(--background-secondary);
border-radius: var(--size-2-3);
padding: var(--size-2-1);
margin-bottom: var(--size-4-4);
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.tg-index-task-source-switcher .fluent-view-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: none;
border: none;
border-radius: 4px;
font-size: 14px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s ease;
}
.tg-index-task-source-switcher .fluent-view-tab:hover {
color: var(--text-normal);
}
.tg-index-task-source-switcher .fluent-view-tab.is-active {
background-color: var(--background-primary);
color: var(--text-normal);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.tg-index-task-source-switcher .fluent-view-tab-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.tg-index-task-source-switcher .fluent-view-tab-label {
display: inline-flex;
align-items: center;
}

View file

@ -433,7 +433,7 @@
.task-genius-settings:has(
.settings-tab-section-active:not([data-tab-id="general"])
)
.settings-tabs-categorized-container {
.settings-tabs-categorized-container {
display: none;
}
@ -444,7 +444,7 @@
/* Show legacy tabs when general tab is active (fallback behavior) */
.task-genius-settings:has(.settings-tab-active[data-tab-id="general"])
.settings-tabs-container {
.settings-tabs-container {
display: grid;
}
@ -456,7 +456,7 @@
.task-genius-settings:has(
.settings-tab-section-active:not([data-tab-id="general"])
)
.task-genius-settings-header {
.task-genius-settings-header {
display: none;
}
@ -1294,8 +1294,7 @@
var(--accent-l),
0.5
);
border: 1px solid
hsl(var(--accent-h), var(--accent-s), var(--accent-l), 0.5);
border: 1px solid hsl(var(--accent-h), var(--accent-s), var(--accent-l), 0.5);
border-radius: var(--radius-m);
color: var(--text-on-accent);
}
@ -1578,3 +1577,24 @@
.task-genius-date-examples-table tr:hover td {
background-color: var(--background-modifier-hover);
}
.tg-index-task-source-wrapper {
margin-top: 18px;
}
.tg-index-task-source-panels {
display: flex;
flex-direction: column;
gap: 12px;
}
.tg-source-settings-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.tg-source-settings-body.tg-source-disabled {
opacity: 0.55;
pointer-events: none;
}

File diff suppressed because one or more lines are too long