feat(quick-capture): add multi-mode capture with file creation support

- Introduce BaseQuickCaptureModal as foundation for modal variants
- Add mode switching between checkbox and file creation strategies
- Implement MinimalQuickCaptureModalWithSwitch and QuickCaptureModalWithSwitch
- Add FileNameInput component for custom file naming
- Enhance settings with mode persistence and file creation options
- Add configuration for keeping modal open after capture
- Support file name templates and default folder locations
- Update UI with improved styling and mode toggle controls

BREAKING CHANGE: Modal instantiation API has changed to support new mode parameter
This commit is contained in:
Quorafind 2025-09-29 20:16:56 +08:00
parent e18ea53b35
commit 09a2e0f2ea
12 changed files with 3231 additions and 110 deletions

View file

@ -282,7 +282,7 @@ export interface QuickCaptureSettings {
placeholder: string;
appendToFile: "append" | "prepend" | "replace";
// New settings for enhanced quick capture
targetType: "fixed" | "daily-note"; // Target type: fixed file or daily note
targetType: "fixed" | "daily-note" | "custom-file"; // Target type: fixed file, daily note, or custom file
targetHeading?: string; // Optional heading to append under
// Daily note settings
dailyNoteSettings: {
@ -298,6 +298,17 @@ export interface QuickCaptureSettings {
minimalModeSettings: {
suggestTrigger: string;
};
// New enhanced settings
keepOpenAfterCapture?: boolean; // Keep modal open after capture
rememberLastMode?: boolean; // Remember the last used mode
lastUsedMode?: "checkbox" | "file"; // Last used save strategy mode
defaultFileNameTemplate?: string; // Default template for file names
defaultFileLocation?: string; // Default folder for new files
createFileMode?: {
defaultFolder: string; // Default folder for file creation
useTemplate: boolean; // Whether to use a template for new files
templateFile: string; // Template file path
};
}
/** Define the structure for task gutter settings */
@ -971,6 +982,17 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
minimalModeSettings: {
suggestTrigger: "/",
},
// New enhanced settings defaults
keepOpenAfterCapture: false,
rememberLastMode: true,
lastUsedMode: "checkbox",
defaultFileNameTemplate: "{{DATE:YYYY-MM-DD}} - ",
defaultFileLocation: "",
createFileMode: {
defaultFolder: "",
useTemplate: false,
templateFile: "",
},
},
// Workflow Defaults

View file

@ -5,6 +5,7 @@ import {
getTodayLocalDateString,
} from "@/utils/date/date-formatter";
import { HabitCard } from "@/components/features/habit/habitcard/habitcard";
import { t } from "@/translations/helper";
import "@/styles/habit.css";
export class HabitChartModal extends Modal {
@ -44,14 +45,14 @@ export class HabitChartModal extends Modal {
let selectedYear: string = String(currentYear);
const rangeSetting = new Setting(controls)
.setName("范围")
.setName(t("Range"))
.addDropdown((dd) => {
dd.addOptions({
all: "全部历史",
"7d": "最近一周",
"30d": "最近一月",
"365d": "最近一年",
year: "整年份",
all: t("All history"),
"7d": t("Last week"),
"30d": t("Last month"),
"365d": t("Last year"),
year: t("Full year"),
});
dd.setValue(selectedRange);
dd.onChange((value) => {
@ -67,7 +68,7 @@ export class HabitChartModal extends Modal {
let yearSelect: HTMLSelectElement = null!;
const yearSetting = new Setting(controls)
.setName("年份")
.setName(t("Year"))
.addDropdown((dd) => {
yearSelect = dd.selectEl;
for (let y = currentYear; y >= earliestYear; y--) {
@ -175,10 +176,10 @@ export class HabitChartModal extends Modal {
let tooltip =
`${dateStr}: ` +
(cellValue == null
? "Missed"
? t("Missed")
: isFilled
? "Completed"
: "Missed");
? t("Completed")
: t("Missed"));
cell.setAttribute("aria-label", tooltip);
if (isInYear) {
cell.addClass(isFilled ? "filled" : "default");
@ -221,10 +222,10 @@ export class HabitChartModal extends Modal {
let tooltip =
`${dateStr}: ` +
(cellValue == null
? "Missed"
? t("Missed")
: isFilled
? "Completed"
: "Missed");
? t("Completed")
: t("Missed"));
cell.setAttribute("aria-label", tooltip);
cell.addClass(
inRange ? (isFilled ? "filled" : "default") : "default"

View file

@ -0,0 +1,226 @@
import {
App,
TFile,
AbstractInputSuggest,
prepareFuzzySearch,
SearchResult,
} from "obsidian";
import { t } from "@/translations/helper";
import { processDateTemplates } from "@/utils/file/file-operations";
/**
* File name suggest for quick capture file creation mode
*/
export class FileNameSuggest extends AbstractInputSuggest<TFile> {
private currentFolder: string = "";
private fileTemplates: string[] = [
"{{DATE:YYYY-MM-DD}} - Meeting Notes",
"{{DATE:YYYY-MM-DD}} - Daily Log",
"{{DATE:YYYY-MM-DD}} - Task List",
"Project - {{DATE:YYYY-MM}}",
"Notes - {{DATE:YYYY-MM-DD-HHmm}}",
];
protected textInputEl: HTMLInputElement;
constructor(app: App, textInputEl: HTMLInputElement, currentFolder?: string) {
super(app, textInputEl);
this.textInputEl = textInputEl;
this.currentFolder = currentFolder || "";
}
/**
* Get suggestions based on input
*/
getSuggestions(inputStr: string): TFile[] {
const files = this.app.vault.getMarkdownFiles();
const lowerInputStr = inputStr.toLowerCase();
const suggestions: TFile[] = [];
// Filter files in current folder
const folderFiles = files.filter((file) => {
if (this.currentFolder && !file.path.startsWith(this.currentFolder)) {
return false;
}
return file.basename.toLowerCase().contains(lowerInputStr);
});
// Add matching files
suggestions.push(...folderFiles.slice(0, 5));
return suggestions;
}
/**
* Render a suggestion
*/
renderSuggestion(file: TFile, el: HTMLElement): void {
el.setText(file.basename);
el.createDiv({
cls: "suggestion-folder",
text: file.parent?.path || "/",
});
}
/**
* Select a suggestion
*/
selectSuggestion(file: TFile): void {
this.textInputEl.value = file.basename;
this.textInputEl.trigger("input");
this.close();
}
}
/**
* File name input component with template support
*/
export class FileNameInput {
private container: HTMLElement;
private inputEl: HTMLInputElement | null = null;
private suggest: FileNameSuggest | null = null;
private templateButtons: HTMLElement | null = null;
private app: App;
private onChange?: (value: string) => void;
constructor(
app: App,
container: HTMLElement,
options?: {
placeholder?: string;
defaultValue?: string;
currentFolder?: string;
onChange?: (value: string) => void;
}
) {
this.app = app;
this.container = container;
this.onChange = options?.onChange;
this.render(options);
}
/**
* Render the component
*/
private render(options?: {
placeholder?: string;
defaultValue?: string;
currentFolder?: string;
}): void {
// Create input container
const inputContainer = this.container.createDiv({
cls: "file-name-input-container",
});
// Label
inputContainer.createEl("label", {
text: t("File Name"),
cls: "file-name-label",
});
// Input field with default template applied
const defaultValue = processDateTemplates(options?.defaultValue || "{{DATE:YYYY-MM-DD}}");
this.inputEl = inputContainer.createEl("input", {
type: "text",
cls: "file-name-input",
placeholder: options?.placeholder || t("Enter file name..."),
value: defaultValue,
});
// Add suggest
this.suggest = new FileNameSuggest(this.app, this.inputEl, options?.currentFolder);
// Template buttons
this.createTemplateButtons();
// Event listeners
this.inputEl.addEventListener("input", () => {
if (this.onChange) {
this.onChange(this.getValue());
}
});
}
/**
* Create template buttons
*/
private createTemplateButtons(): void {
this.templateButtons = this.container.createDiv({
cls: "file-name-templates",
});
const templatesLabel = this.templateButtons.createDiv({
cls: "templates-label",
text: t("Quick Templates:"),
});
const buttonContainer = this.templateButtons.createDiv({
cls: "template-buttons",
});
const templates = [
{ label: t("Today's Note"), template: "{{DATE:YYYY-MM-DD}}" },
{ label: t("Meeting"), template: "{{DATE:YYYY-MM-DD}} - Meeting" },
{ label: t("Project"), template: "Project - {{DATE:YYYY-MM}}" },
{ label: t("Task List"), template: "{{DATE:YYYY-MM-DD}} - Tasks" },
];
templates.forEach((tmpl) => {
const button = buttonContainer.createEl("button", {
text: tmpl.label,
cls: "template-button",
});
button.addEventListener("click", () => {
if (this.inputEl) {
// Process template immediately
const processedValue = processDateTemplates(tmpl.template);
this.inputEl.value = processedValue;
if (this.onChange) {
this.onChange(this.getValue());
}
this.inputEl.focus();
}
});
});
}
/**
* Get the current value
*/
getValue(): string {
if (!this.inputEl) return "";
// Just return the value directly since templates are already processed
return this.inputEl.value;
}
/**
* Set the value
*/
setValue(value: string): void {
if (this.inputEl) {
this.inputEl.value = value;
}
}
/**
* Clear the input
*/
clear(): void {
this.setValue("");
}
/**
* Focus the input
*/
focus(): void {
this.inputEl?.focus();
}
/**
* Destroy the component
*/
destroy(): void {
this.suggest?.close();
this.container.empty();
}
}

View file

@ -0,0 +1,625 @@
import {
App,
Modal,
TFile,
Notice,
moment,
ButtonComponent,
setIcon,
} from "obsidian";
import TaskProgressBarPlugin from "@/index";
import {
saveCapture,
processDateTemplates,
} from "@/utils/file/file-operations";
import { t } from "@/translations/helper";
import { SuggestManager } from "@/components/ui/suggest";
import { EmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
/**
* Quick capture save strategy types
*/
export type QuickCaptureMode = "checkbox" | "file";
/**
* Task metadata interface
*/
export interface TaskMetadata {
startDate?: Date;
dueDate?: Date;
scheduledDate?: Date;
priority?: number;
project?: string;
context?: string;
recurrence?: string;
status?: string;
tags?: string[];
location?: "fixed" | "daily-note" | "file";
targetFile?: string;
customFileName?: string;
// Track which fields were manually set by user
manuallySet?: {
startDate?: boolean;
dueDate?: boolean;
scheduledDate?: boolean;
};
}
/**
* Base class for all Quick Capture modals
* Provides shared functionality and state management
*/
export abstract class BaseQuickCaptureModal extends Modal {
plugin: TaskProgressBarPlugin;
protected markdownEditor: EmbeddableMarkdownEditor | null = null;
protected capturedContent: string = "";
protected taskMetadata: TaskMetadata = {};
protected currentMode: QuickCaptureMode;
protected suggestManager: SuggestManager;
// UI Elements
protected headerContainer: HTMLElement | null = null;
protected contentContainer: HTMLElement | null = null;
protected footerContainer: HTMLElement | null = null;
// Settings
protected tempTargetFilePath: string = "";
protected preferMetadataFormat: "dataview" | "tasks" = "tasks";
protected keepOpenAfterCapture: boolean = false;
constructor(
app: App,
plugin: TaskProgressBarPlugin,
initialMode: QuickCaptureMode = "checkbox",
metadata?: TaskMetadata
) {
super(app);
this.plugin = plugin;
this.currentMode = initialMode;
// Initialize suggest manager
this.suggestManager = new SuggestManager(app, plugin);
// Initialize metadata
if (metadata) {
this.taskMetadata = metadata;
// Auto-switch to file mode if location is file
if (metadata.location === "file") {
this.currentMode = "file";
}
}
// If FileSource is disabled, force mode to checkbox
if (
this.currentMode === "file" &&
!this.plugin.settings?.fileSource?.enabled
) {
this.currentMode = "checkbox";
}
// Initialize settings
this.preferMetadataFormat = this.plugin.settings.preferMetadataFormat;
this.keepOpenAfterCapture =
this.plugin.settings.quickCapture.keepOpenAfterCapture || false;
// Initialize target file path
this.initializeTargetFile();
}
/**
* Initialize target file based on settings
*/
protected initializeTargetFile(): void {
const settings = this.plugin.settings.quickCapture;
if (
this.taskMetadata.location === "file" &&
this.taskMetadata.customFileName
) {
this.tempTargetFilePath = this.taskMetadata.customFileName;
} else if (settings.targetType === "daily-note") {
const dateStr = moment().format(settings.dailyNoteSettings.format);
const pathWithDate = settings.dailyNoteSettings.folder
? `${settings.dailyNoteSettings.folder}/${dateStr}.md`
: `${dateStr}.md`;
this.tempTargetFilePath = this.sanitizeFilePath(pathWithDate);
} else {
this.tempTargetFilePath = settings.targetFile;
}
}
/**
* Called when the modal is opened
*/
onOpen() {
const { contentEl } = this;
this.modalEl.toggleClass("quick-capture-modal", true);
this.modalEl.toggleClass(`quick-capture-${this.currentMode}`, true);
// Start managing suggests
this.suggestManager.startManaging();
// Create base UI structure
this.createBaseUI(contentEl);
// Let subclasses create their UI (should create editor container)
this.createUI();
// Initialize editor and other components after UI is created
this.initializeComponents();
}
/**
* Create base UI structure
*/
protected createBaseUI(contentEl: HTMLElement): void {
// Create header container
this.titleEl.toggleClass("quick-capture-header", true);
this.headerContainer = this.titleEl;
this.createHeader();
// Create content container
this.contentContainer = contentEl.createDiv({
cls: "quick-capture-content",
});
// Create footer container
this.footerContainer = contentEl.createDiv({
cls: "quick-capture-footer",
});
this.createFooter();
}
/**
* Create header with save strategy switcher and clear button
*/
protected createHeader(): void {
if (!this.headerContainer) return;
// Left side: Save strategy tabs with ARIA attributes
const tabContainer = this.headerContainer.createDiv({
cls: "quick-capture-tabs",
attr: {
role: "tablist",
"aria-label": t("Save mode selection"),
},
});
// Checkbox mode button (save as checkbox task)
const checkboxButton = new ButtonComponent(tabContainer)
.setClass("quick-capture-tab")
.onClick(() => this.switchMode("checkbox"));
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");
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",
});
// File mode button (save as file) - only when FileSource is enabled
if (this.plugin.settings?.fileSource?.enabled) {
const fileButton = new ButtonComponent(tabContainer)
.setClass("quick-capture-tab")
.onClick(() => this.switchMode("file"));
fileButton.buttonEl.toggleClass("clickable-icon", true);
const fileButtonEl = fileButton.buttonEl;
fileButtonEl.setAttribute("role", "tab");
fileButtonEl.setAttribute(
"aria-selected",
String(this.currentMode === "file")
);
fileButtonEl.setAttribute("aria-controls", "quick-capture-content");
fileButtonEl.setAttribute("data-mode", "file");
if (this.currentMode === "file") {
fileButton.setClass("active");
}
fileButtonEl.empty();
const fileIconSpan = fileButtonEl.createSpan({
cls: "quick-capture-tab-icon",
});
setIcon(fileIconSpan, "file-plus");
fileButtonEl.createSpan({
text: t("File"),
cls: "quick-capture-tab-text",
});
}
// Right side: Clear button with improved styling
const clearButton = this.headerContainer.createEl("button", {
text: t("Clear"),
cls: ["quick-capture-clear", "clickable-icon"],
attr: {
"aria-label": t("Clear all content"),
type: "button",
},
});
clearButton.addEventListener("click", () => this.handleClear());
}
/**
* Create footer with continue and action buttons
*/
protected createFooter(): void {
if (!this.footerContainer) return;
// Left side: Continue creating button
const leftContainer = this.footerContainer.createDiv({
cls: "quick-capture-footer-left",
});
const continueButton = leftContainer.createEl("button", {
text: t("Continue & New"),
cls: "quick-capture-continue",
});
continueButton.addEventListener("click", () =>
this.handleContinueCreate()
);
// Right side: Main action buttons
const rightContainer = this.footerContainer.createDiv({
cls: "quick-capture-footer-right",
});
// Save/Create button
const submitButton = rightContainer.createEl("button", {
text:
this.currentMode === "file" ? t("Save as File") : t("Add Task"),
cls: "mod-cta",
});
submitButton.addEventListener("click", () => this.handleSubmit());
// Cancel button
const cancelButton = rightContainer.createEl("button", {
text: t("Cancel"),
});
cancelButton.addEventListener("click", () => this.close());
}
/**
* Switch to a different mode
*/
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) {
new Notice(
t(
"File Task is disabled. Enable FileSource in Settings to use File mode."
)
);
return;
}
// Save current state
const savedContent = this.capturedContent;
const savedMetadata = { ...this.taskMetadata };
// Update mode
this.currentMode = mode;
// Update modal classes
this.modalEl.removeClass(
"quick-capture-checkbox",
"quick-capture-file"
);
this.modalEl.addClass(`quick-capture-${mode}`);
// Update tab active states
const tabs =
this.headerContainer?.querySelectorAll(".quick-capture-tab");
tabs?.forEach((tab, index) => {
tab.removeClass("active");
if (
(index === 0 && mode === "checkbox") ||
(index === 1 && mode === "file")
) {
tab.addClass("active");
}
});
// Update only the target display instead of recreating everything
this.updateTargetDisplay();
// Restore metadata
this.taskMetadata = savedMetadata;
// Update button text
const submitButton = this.footerContainer?.querySelector(
".mod-cta"
) as HTMLButtonElement;
if (submitButton) {
submitButton.setText(
mode === "file" ? t("Save as File") : t("Add Task")
);
}
}
/**
* Handle clear action
*/
protected handleClear(): void {
// Clear content
this.capturedContent = "";
if (this.markdownEditor) {
this.markdownEditor.set("", false);
}
// Clear metadata but keep location settings
const location = this.taskMetadata.location;
const targetFile = this.taskMetadata.targetFile;
this.taskMetadata = {
location,
targetFile,
};
// Reset any UI elements
this.resetUIElements();
// Focus editor
this.markdownEditor?.editor?.focus();
}
/**
* Handle continue & create new
*/
protected async handleContinueCreate(): Promise<void> {
// First, save the current task
const content =
this.capturedContent.trim() ||
this.markdownEditor?.value.trim() ||
"";
if (!content) {
new Notice(t("Nothing to capture"));
return;
}
try {
await this.saveContent(content);
new Notice(t("Captured successfully"));
// Clear for next task but keep settings
this.handleClear();
} catch (error) {
new Notice(`${t("Failed to save:")} ${error}`);
}
}
/**
* Handle submit action
*/
protected async handleSubmit(): Promise<void> {
const content =
this.capturedContent.trim() ||
this.markdownEditor?.value.trim() ||
"";
if (!content) {
new Notice(t("Nothing to capture"));
return;
}
try {
await this.saveContent(content);
new Notice(
this.currentMode === "file"
? t("File saved successfully")
: t("Task added successfully")
);
if (!this.keepOpenAfterCapture) {
this.close();
} else {
this.handleClear();
}
} catch (error) {
new Notice(`${t("Failed to save:")} ${error}`);
}
}
/**
* Save content based on current mode and settings
*/
protected async saveContent(content: string): Promise<void> {
let processedContent = this.processContentWithMetadata(content);
let targetFile = this.tempTargetFilePath;
// Handle file mode
if (this.currentMode === "file" && this.taskMetadata.customFileName) {
targetFile = processDateTemplates(this.taskMetadata.customFileName);
if (!targetFile.endsWith(".md")) {
targetFile += ".md";
}
// Prefix default folder when configured and FileSource is enabled
const defaultFolder =
this.plugin.settings.quickCapture.createFileMode?.defaultFolder?.trim();
if (
this.plugin.settings?.fileSource?.enabled &&
defaultFolder &&
!targetFile.includes("/")
) {
targetFile = this.sanitizeFilePath(
`${defaultFolder}/${targetFile}`
);
}
// Inject frontmatter (and tags) for File mode
const useTemplate =
!!this.plugin.settings.quickCapture.createFileMode?.useTemplate;
const hasFrontmatter = processedContent
.trimStart()
.startsWith("---");
if (useTemplate) {
// Only ensure frontmatter exists
if (!hasFrontmatter) {
processedContent = `---\nstatus: \" \"\n---\n\n${processedContent}`;
}
} else {
// Inject minimal frontmatter; include tags only if FileSource tag recognition is enabled and configured
if (!hasFrontmatter) {
const tagCfg =
this.plugin.settings?.fileSource?.recognitionStrategies
?.tags;
const tagRecogEnabled = !!tagCfg?.enabled;
const userTags: string[] = Array.isArray(tagCfg?.taskTags)
? tagCfg!.taskTags
: [];
const yamlTags = userTags
.map((t) =>
typeof t === "string" ? t.replace(/^#/, "") : ""
)
.filter((t) => !!t);
if (tagRecogEnabled && yamlTags.length > 0) {
const tagsLine = `tags: [${yamlTags
.map((t) => JSON.stringify(t))
.join(", ")}]`;
processedContent = `---\nstatus: \" \"\n${tagsLine}\n---\n\n${processedContent}`;
} else {
processedContent = `---\nstatus: \" \"\n---\n\n${processedContent}`;
}
}
}
}
// Convert location/targetType to valid QuickCaptureOptions type
let targetType: "fixed" | "daily-note" | undefined = "fixed";
if (this.taskMetadata.location === "daily-note") {
targetType = "daily-note";
} else if (
this.taskMetadata.location === "file" ||
this.taskMetadata.location === "fixed"
) {
targetType = "fixed";
} else if (
this.plugin.settings.quickCapture.targetType === "daily-note"
) {
targetType = "daily-note";
} else {
targetType = "fixed"; // Default to fixed for custom-file or any other type
}
const captureOptions = {
...this.plugin.settings.quickCapture,
targetFile,
targetType,
// For file mode, always replace
appendToFile:
this.currentMode === "file"
? ("replace" as const)
: this.plugin.settings.quickCapture.appendToFile,
};
await saveCapture(this.app, processedContent, captureOptions);
}
/**
* Process content with metadata
*/
protected abstract processContentWithMetadata(content: string): string;
/**
* Create UI - subclasses should create their UI here
*/
protected abstract createUI(): void;
/**
* Update target display when mode changes
*/
protected abstract updateTargetDisplay(): void;
/**
* Initialize components after UI creation
*/
protected abstract initializeComponents(): void;
/**
* Reset UI elements to default state
*/
protected abstract resetUIElements(): void;
/**
* Sanitize file path
*/
protected sanitizeFilePath(filePath: string): string {
const pathParts = filePath.split("/");
const sanitizedParts = pathParts.map((part, index) => {
if (index === pathParts.length - 1) {
return this.sanitizeFilename(part);
}
return part
.replace(/[<>:"|*?\\]/g, "-")
.replace(/\s+/g, " ")
.trim();
});
return sanitizedParts.join("/");
}
/**
* Sanitize filename
*/
protected sanitizeFilename(filename: string): string {
return filename
.replace(/[<>:"|*?\\]/g, "-")
.replace(/\s+/g, " ")
.trim();
}
/**
* Format date
*/
protected formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
)}-${String(date.getDate()).padStart(2, "0")}`;
}
/**
* Parse date
*/
protected parseDate(dateString: string): Date {
const [year, month, day] = dateString.split("-").map(Number);
return new Date(year, month - 1, day);
}
/**
* Called when the modal is closed
*/
onClose() {
// Stop managing suggests
this.suggestManager.stopManaging();
// Clean up editor
if (this.markdownEditor) {
this.markdownEditor.destroy();
this.markdownEditor = null;
}
// Clear content
this.contentEl.empty();
}
}

View file

@ -11,14 +11,17 @@ import {
import {
createEmbeddableMarkdownEditor,
EmbeddableMarkdownEditor,
} from '@/editor-extensions/core/markdown-editor';
import TaskProgressBarPlugin from '@/index';
import { saveCapture } from '@/utils/file/file-operations';
import { t } from '@/translations/helper';
import { MinimalQuickCaptureSuggest } from '@/components/features/quick-capture/suggest/MinimalQuickCaptureSuggest';
import { SuggestManager, UniversalEditorSuggest } from '@/components/ui/suggest';
import { ConfigurableTaskParser } from '@/dataflow/core/ConfigurableTaskParser';
import { clearAllMarks } from '@/components/ui/renderers/MarkdownRenderer';
} from "@/editor-extensions/core/markdown-editor";
import TaskProgressBarPlugin from "@/index";
import { saveCapture } from "@/utils/file/file-operations";
import { t } from "@/translations/helper";
import { MinimalQuickCaptureSuggest } from "@/components/features/quick-capture/suggest/MinimalQuickCaptureSuggest";
import {
SuggestManager,
UniversalEditorSuggest,
} from "@/components/ui/suggest";
import { ConfigurableTaskParser } from "@/dataflow/core/ConfigurableTaskParser";
import { clearAllMarks } from "@/components/ui/renderers/MarkdownRenderer";
interface TaskMetadata {
startDate?: Date;
@ -28,7 +31,7 @@ interface TaskMetadata {
project?: string;
context?: string;
tags?: string[];
location?: "fixed" | "daily-note";
location?: "fixed" | "daily-note" | "custom-file";
targetFile?: string;
}
@ -320,11 +323,7 @@ export class MinimalQuickCaptureModal extends Modal {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.dateButton) {
const rect = this.dateButton.getBoundingClientRect();
this.showMenuAtCoords(
menu,
rect.left,
rect.bottom + 5
);
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
@ -363,11 +362,7 @@ export class MinimalQuickCaptureModal extends Modal {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.priorityButton) {
const rect = this.priorityButton.getBoundingClientRect();
this.showMenuAtCoords(
menu,
rect.left,
rect.bottom + 5
);
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
@ -424,11 +419,7 @@ export class MinimalQuickCaptureModal extends Modal {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.locationButton) {
const rect = this.locationButton.getBoundingClientRect();
this.showMenuAtCoords(
menu,
rect.left,
rect.bottom + 5
);
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
@ -583,10 +574,28 @@ export class MinimalQuickCaptureModal extends Modal {
const content = this.capturedContent.trim();
if (!content) {
// Update button states based on existing taskMetadata
this.updateButtonState(this.dateButton!, !!this.taskMetadata.dueDate);
this.updateButtonState(this.priorityButton!, !!this.taskMetadata.priority);
this.updateButtonState(this.tagButton!, !!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0));
this.updateButtonState(this.locationButton!, !!(this.taskMetadata.location || this.taskMetadata.targetFile));
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(
this.taskMetadata.tags &&
this.taskMetadata.tags.length > 0
)
);
this.updateButtonState(
this.locationButton!,
!!(
this.taskMetadata.location ||
this.taskMetadata.targetFile
)
);
return;
}
@ -596,11 +605,12 @@ export class MinimalQuickCaptureModal extends Modal {
});
// Extract metadata and tags
const [cleanedContent, metadata, tags] = parser.extractMetadataAndTags(content);
const [cleanedContent, metadata, tags] =
parser.extractMetadataAndTags(content);
// Only update taskMetadata if we found actual marks in the content
// This preserves manually set values from suggest system
// Due date - only update if found in content
if (metadata.dueDate) {
this.taskMetadata.dueDate = new Date(metadata.dueDate);
@ -610,13 +620,14 @@ export class MinimalQuickCaptureModal extends Modal {
// Priority - only update if found in content
if (metadata.priority) {
const priorityMap: Record<string, number> = {
"highest": 5,
"high": 4,
"medium": 3,
"low": 2,
"lowest": 1
highest: 5,
high: 4,
medium: 3,
low: 2,
lowest: 1,
};
this.taskMetadata.priority = priorityMap[metadata.priority] || 3;
this.taskMetadata.priority =
priorityMap[metadata.priority] || 3;
}
// Don't delete existing priority if not found in content
@ -626,7 +637,7 @@ export class MinimalQuickCaptureModal extends Modal {
this.taskMetadata.tags = [];
}
// Merge new tags with existing ones, avoid duplicates
tags.forEach(tag => {
tags.forEach((tag) => {
if (!this.taskMetadata.tags!.includes(tag)) {
this.taskMetadata.tags!.push(tag);
}
@ -634,18 +645,46 @@ export class MinimalQuickCaptureModal extends Modal {
}
// Update button states based on current taskMetadata
this.updateButtonState(this.dateButton!, !!this.taskMetadata.dueDate);
this.updateButtonState(this.priorityButton!, !!this.taskMetadata.priority);
this.updateButtonState(this.tagButton!, !!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0));
this.updateButtonState(this.locationButton!, !!(this.taskMetadata.location || this.taskMetadata.targetFile || metadata.project || metadata.location));
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
);
this.updateButtonState(
this.locationButton!,
!!(
this.taskMetadata.location ||
this.taskMetadata.targetFile ||
metadata.project ||
metadata.location
)
);
} catch (error) {
console.error("Error parsing content:", error);
// On error, still update button states based on existing taskMetadata
this.updateButtonState(this.dateButton!, !!this.taskMetadata.dueDate);
this.updateButtonState(this.priorityButton!, !!this.taskMetadata.priority);
this.updateButtonState(this.tagButton!, !!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0));
this.updateButtonState(this.locationButton!, !!(this.taskMetadata.location || this.taskMetadata.targetFile));
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
);
this.updateButtonState(
this.locationButton!,
!!(this.taskMetadata.location || this.taskMetadata.targetFile)
);
}
}
}

View file

@ -0,0 +1,731 @@
import { App, Notice, Menu, setIcon, EditorPosition } from "obsidian";
import { createEmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
import TaskProgressBarPlugin from "@/index";
import { t } from "@/translations/helper";
import { MinimalQuickCaptureSuggest } from "@/components/features/quick-capture/suggest/MinimalQuickCaptureSuggest";
import { UniversalEditorSuggest } from "@/components/ui/suggest";
import { ConfigurableTaskParser } from "@/dataflow/core/ConfigurableTaskParser";
import { clearAllMarks } from "@/components/ui/renderers/MarkdownRenderer";
import {
BaseQuickCaptureModal,
QuickCaptureMode,
TaskMetadata,
} from "./BaseQuickCaptureModal";
import { moment } from "obsidian";
/**
* Minimal Quick Capture Modal extending the base class
*/
export class MinimalQuickCaptureModal extends BaseQuickCaptureModal {
// UI Elements
private dateButton: HTMLButtonElement | null = null;
private priorityButton: HTMLButtonElement | null = null;
private locationButton: HTMLButtonElement | null = null;
private tagButton: HTMLButtonElement | null = null;
// Suggest instances
private minimalSuggest: MinimalQuickCaptureSuggest;
private universalSuggest: UniversalEditorSuggest | null = null;
// UI element references
private targetIndicator: HTMLElement | null = null;
private fileNameInput: HTMLInputElement | null = null;
private editorContainer: HTMLElement | null = null;
constructor(app: App, plugin: TaskProgressBarPlugin) {
// Default to checkbox mode for task creation
super(app, plugin, "checkbox");
this.minimalSuggest = plugin.minimalQuickCaptureSuggest;
// Initialize default metadata for checkbox mode
const targetType = this.plugin.settings.quickCapture.targetType;
this.taskMetadata.location =
(targetType === "custom-file" ? "file" : targetType) || "fixed";
this.taskMetadata.targetFile = this.getTargetFile();
}
onOpen() {
// Store modal instance reference for suggest system
(this.modalEl as any).__minimalQuickCaptureModal = this;
// Set up the suggest system
if (this.minimalSuggest) {
this.minimalSuggest.setMinimalMode(true);
}
super.onOpen();
}
/**
* Initialize components after UI creation
*/
protected initializeComponents(): void {
// Setup markdown editor only if not already initialized
if (this.contentContainer && !this.markdownEditor) {
const editorContainer = this.contentContainer.querySelector(
".quick-capture-minimal-editor"
) as HTMLElement;
if (editorContainer) {
this.setupMarkdownEditor(editorContainer);
}
// Enable universal suggest for minimal modal after editor is created
setTimeout(() => {
if (this.markdownEditor?.editor?.editor) {
this.universalSuggest =
this.suggestManager.enableForMinimalModal(
this.markdownEditor.editor.editor
);
this.universalSuggest.enable();
}
}, 100);
}
// Restore content if exists
if (this.markdownEditor && this.capturedContent) {
this.markdownEditor.set(this.capturedContent, false);
}
}
/**
* Create UI - consistent layout for both modes
*/
protected createUI(): void {
if (!this.contentContainer) return;
// Target indicator (shows destination or file name)
const targetContainer = this.contentContainer.createDiv({
cls: "quick-capture-minimal-target-container",
});
this.targetIndicator = targetContainer.createDiv({
cls: "quick-capture-minimal-target",
});
// Editor container - same for both modes
const editorWrapper = this.contentContainer.createDiv({
cls: "quick-capture-minimal-editor-container",
});
this.editorContainer = editorWrapper.createDiv({
cls: "quick-capture-modal-editor quick-capture-minimal-editor",
});
// Quick action buttons container (only for checkbox mode)
const buttonsContainer = this.contentContainer.createDiv({
cls: "quick-capture-minimal-quick-actions",
});
this.createQuickActionButtons(buttonsContainer);
// Update target display based on initial mode
this.updateTargetDisplay();
}
/**
* Update target display based on current mode
*/
protected updateTargetDisplay(): void {
if (!this.targetIndicator) return;
this.targetIndicator.empty();
if (this.currentMode === "checkbox") {
// Show target file for checkbox mode
const label = this.targetIndicator.createSpan({
cls: "quick-capture-target-label",
text: t("To: "),
});
const target = this.targetIndicator.createSpan({
cls: "quick-capture-target-value",
text: this.tempTargetFilePath,
});
// Show quick action buttons
const buttonsContainer = this.contentContainer?.querySelector(".quick-capture-minimal-quick-actions");
if (buttonsContainer) {
(buttonsContainer as HTMLElement).style.display = "flex";
}
} else {
// Show file name input for file mode
const label = this.targetIndicator.createSpan({
cls: "quick-capture-target-label",
text: t("Save as: "),
});
this.fileNameInput = this.targetIndicator.createEl("input", {
cls: "quick-capture-minimal-file-input",
attr: {
type: "text",
placeholder: t("Enter file name..."),
value: this.taskMetadata.customFileName || this.plugin.settings.quickCapture.defaultFileNameTemplate || "{{DATE:YYYY-MM-DD}} - ",
},
});
// Update the customFileName when input changes
this.fileNameInput.addEventListener("input", () => {
if (this.fileNameInput) {
this.taskMetadata.customFileName = this.fileNameInput.value;
}
});
// Hide quick action buttons
const buttonsContainer = this.contentContainer?.querySelector(".quick-capture-minimal-quick-actions");
if (buttonsContainer) {
(buttonsContainer as HTMLElement).style.display = "none";
}
}
}
/**
* Create quick action buttons
*/
private createQuickActionButtons(container: HTMLElement): void {
const leftContainer = container.createDiv({
cls: "quick-actions-left",
});
this.dateButton = leftContainer.createEl("button", {
cls: ["quick-action-button", "clickable-icon"],
attr: { "aria-label": t("Set date") },
});
setIcon(this.dateButton, "calendar");
this.dateButton.addEventListener("click", () => this.showDatePicker());
this.updateButtonState(this.dateButton, !!this.taskMetadata.dueDate);
this.priorityButton = leftContainer.createEl("button", {
cls: ["quick-action-button", "clickable-icon"],
attr: { "aria-label": t("Set priority") },
});
setIcon(this.priorityButton, "zap");
this.priorityButton.addEventListener("click", () =>
this.showPriorityMenu()
);
this.updateButtonState(
this.priorityButton,
!!this.taskMetadata.priority
);
this.locationButton = leftContainer.createEl("button", {
cls: ["quick-action-button", "clickable-icon"],
attr: { "aria-label": t("Set location") },
});
setIcon(this.locationButton, "folder");
this.locationButton.addEventListener("click", () =>
this.showLocationMenu()
);
this.updateButtonState(
this.locationButton,
this.taskMetadata.location !==
(this.plugin.settings.quickCapture.targetType || "fixed")
);
this.tagButton = leftContainer.createEl("button", {
cls: ["quick-action-button", "clickable-icon"],
attr: { "aria-label": t("Add tags") },
});
setIcon(this.tagButton, "tag");
this.tagButton.addEventListener("click", () => {});
this.updateButtonState(
this.tagButton,
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
);
}
/**
* Setup markdown editor
*/
private setupMarkdownEditor(container: HTMLElement): void {
setTimeout(() => {
this.markdownEditor = createEmbeddableMarkdownEditor(
this.app,
container,
{
placeholder: t("Enter your content..."),
singleLine: false, // Allow multiline for both modes
onEnter: (editor, mod, shift) => {
// Cmd/Ctrl+Enter always submits
if (mod) {
this.handleSubmit();
return true;
}
// In checkbox mode, plain Enter also submits for quick capture
if (this.currentMode === "checkbox" && !shift) {
this.handleSubmit();
return true;
}
// In file mode or shift+enter, create new line
return false;
},
onEscape: (editor) => {
this.close();
},
onChange: (update) => {
this.capturedContent = this.markdownEditor?.value || "";
// Parse content and update button states only in checkbox mode
if (this.currentMode === "checkbox") {
this.parseContentAndUpdateButtons();
}
},
}
);
// Focus the editor
this.markdownEditor?.editor?.focus();
// Restore content if exists
if (this.capturedContent && this.markdownEditor) {
this.markdownEditor.set(this.capturedContent, false);
}
}, 50);
}
/**
* Update button state
*/
private updateButtonState(
button: HTMLButtonElement,
isActive: boolean
): void {
if (isActive) {
button.addClass("active");
} else {
button.removeClass("active");
}
}
/**
* Show menu at specified coordinates
*/
private showMenuAtCoords(menu: Menu, x: number, y: number): void {
menu.showAtMouseEvent(
new MouseEvent("click", {
clientX: x,
clientY: y,
})
);
}
// Date picker methods
public showDatePickerAtCursor(cursorCoords: any, cursor: EditorPosition) {
this.showDatePicker(cursor, cursorCoords);
}
public showDatePicker(cursor?: EditorPosition, coords?: any) {
const quickDates = [
{ label: t("Tomorrow"), date: moment().add(1, "day").toDate() },
{
label: t("Day after tomorrow"),
date: moment().add(2, "day").toDate(),
},
{ label: t("Next week"), date: moment().add(1, "week").toDate() },
{ label: t("Next month"), date: moment().add(1, "month").toDate() },
];
const menu = new Menu();
quickDates.forEach((quickDate) => {
menu.addItem((item) => {
item.setTitle(quickDate.label);
item.setIcon("calendar");
item.onClick(() => {
this.taskMetadata.dueDate = quickDate.date;
this.updateButtonState(this.dateButton!, true);
// If called from suggest, replace the ~ with date text
if (cursor && this.markdownEditor) {
this.replaceAtCursor(
cursor,
this.formatDate(quickDate.date)
);
}
});
});
});
menu.addSeparator();
menu.addItem((item) => {
item.setTitle(t("Choose date..."));
item.setIcon("calendar-days");
item.onClick(() => {
// TODO: Implement full date picker integration
});
});
// Show menu
if (coords) {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.dateButton) {
const rect = this.dateButton.getBoundingClientRect();
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
// Priority menu methods
public showPriorityMenuAtCursor(cursorCoords: any, cursor: EditorPosition) {
this.showPriorityMenu(cursor, cursorCoords);
}
public showPriorityMenu(cursor?: EditorPosition, coords?: any) {
const priorities = [
{ level: 5, label: t("Highest"), icon: "🔺" },
{ level: 4, label: t("High"), icon: "⏫" },
{ level: 3, label: t("Medium"), icon: "🔼" },
{ level: 2, label: t("Low"), icon: "🔽" },
{ level: 1, label: t("Lowest"), icon: "⏬" },
];
const menu = new Menu();
priorities.forEach((priority) => {
menu.addItem((item) => {
item.setTitle(`${priority.icon} ${priority.label}`);
item.onClick(() => {
this.taskMetadata.priority = priority.level;
this.updateButtonState(this.priorityButton!, true);
// If called from suggest, replace the ! with priority icon
if (cursor && this.markdownEditor) {
this.replaceAtCursor(cursor, priority.icon);
}
});
});
});
// Show menu
if (coords) {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.priorityButton) {
const rect = this.priorityButton.getBoundingClientRect();
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
// Location menu methods
public showLocationMenuAtCursor(cursorCoords: any, cursor: EditorPosition) {
this.showLocationMenu(cursor, cursorCoords);
}
public showLocationMenu(cursor?: EditorPosition, coords?: any) {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle(t("Fixed location"));
item.setIcon("file");
item.onClick(() => {
this.taskMetadata.location = "fixed";
this.taskMetadata.targetFile =
this.plugin.settings.quickCapture.targetFile;
this.updateButtonState(
this.locationButton!,
this.taskMetadata.location !==
(this.plugin.settings.quickCapture.targetType ||
"fixed")
);
// If called from suggest, replace the 📁 with file text
if (cursor && this.markdownEditor) {
this.replaceAtCursor(cursor, t("Fixed location"));
}
});
});
menu.addItem((item) => {
item.setTitle(t("Daily note"));
item.setIcon("calendar");
item.onClick(() => {
this.taskMetadata.location = "daily-note";
this.taskMetadata.targetFile = this.getDailyNoteFile();
this.updateButtonState(
this.locationButton!,
this.taskMetadata.location !==
(this.plugin.settings.quickCapture?.targetType ||
"fixed")
);
// If called from suggest, replace the 📁 with daily note text
if (cursor && this.markdownEditor) {
this.replaceAtCursor(cursor, t("Daily note"));
}
});
});
// Only show custom file option when FileSource is enabled
if (this.plugin.settings?.fileSource?.enabled) {
menu.addItem((item) => {
item.setTitle(t("Custom file"));
item.setIcon("file-plus");
item.onClick(() => {
this.taskMetadata.location = "file";
// Switch to file mode for custom file creation
this.switchMode("file");
// If called from suggest, replace the 📁 with custom file text
if (cursor && this.markdownEditor) {
this.replaceAtCursor(cursor, t("Custom file"));
}
});
});
}
// Show menu
if (coords) {
this.showMenuAtCoords(menu, coords.left, coords.top);
} else if (this.locationButton) {
const rect = this.locationButton.getBoundingClientRect();
this.showMenuAtCoords(menu, rect.left, rect.bottom + 5);
}
}
// Tag selector methods
public showTagSelectorAtCursor(cursorCoords: any, cursor: EditorPosition) {
// TODO: Implement tag selector
}
/**
* Replace text at cursor position
*/
private replaceAtCursor(cursor: EditorPosition, replacement: string) {
if (!this.markdownEditor) return;
// Replace the character at cursor position using CodeMirror API
const cm = (this.markdownEditor.editor as any).cm;
if (cm && cm.replaceRange) {
cm.replaceRange(
replacement,
{ line: cursor.line, ch: cursor.ch - 1 },
cursor
);
}
}
/**
* Get target file
*/
private getTargetFile(): string {
const settings = this.plugin.settings.quickCapture;
if (this.taskMetadata.location === "daily-note") {
return this.getDailyNoteFile();
}
return settings.targetFile;
}
/**
* Get daily note file
*/
private getDailyNoteFile(): string {
const settings = this.plugin.settings.quickCapture.dailyNoteSettings;
const dateStr = moment().format(settings.format);
return settings.folder
? `${settings.folder}/${dateStr}.md`
: `${dateStr}.md`;
}
/**
* Process content with metadata based on save strategy
*/
protected processContentWithMetadata(content: string): string {
if (!content.trim()) return "";
// For file mode, just return content as-is
if (this.currentMode === "file") {
return content;
}
// For checkbox mode, format as tasks
const lines = content.split("\n");
const processedLines = lines.map((line) => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("- [")) {
// Use clearAllMarks to completely clean the content
const cleanedContent = clearAllMarks(trimmed);
return `- [ ] ${cleanedContent}`;
}
return line;
});
// Add metadata for checkbox mode
return this.addMetadataToContent(processedLines.join("\n"));
}
/**
* Add metadata to content
*/
private addMetadataToContent(content: string): string {
const metadata: string[] = [];
// Add date metadata
if (this.taskMetadata.dueDate) {
metadata.push(`📅 ${this.formatDate(this.taskMetadata.dueDate)}`);
}
// Add priority metadata
if (this.taskMetadata.priority) {
const priorityIcons = ["⏬", "🔽", "🔼", "⏫", "🔺"];
metadata.push(priorityIcons[this.taskMetadata.priority - 1]);
}
// Add tags
if (this.taskMetadata.tags && this.taskMetadata.tags.length > 0) {
metadata.push(...this.taskMetadata.tags.map((tag) => `#${tag}`));
}
// Add metadata to content
if (metadata.length > 0) {
return `${content} ${metadata.join(" ")}`;
}
return content;
}
/**
* Parse content and update button states
*/
public parseContentAndUpdateButtons(): void {
try {
const content = this.capturedContent.trim();
if (!content) {
// Update button states based on existing taskMetadata
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(
this.taskMetadata.tags &&
this.taskMetadata.tags.length > 0
)
);
this.updateButtonState(
this.locationButton!,
!!(
this.taskMetadata.location ||
this.taskMetadata.targetFile
)
);
return;
}
// Create a parser to extract metadata
const parser = new ConfigurableTaskParser({});
// Extract metadata and tags
const [cleanedContent, metadata, tags] =
parser.extractMetadataAndTags(content);
// Update taskMetadata based on parsed content
if (metadata.dueDate) {
this.taskMetadata.dueDate = new Date(metadata.dueDate);
}
if (metadata.priority) {
const priorityMap: Record<string, number> = {
highest: 5,
high: 4,
medium: 3,
low: 2,
lowest: 1,
};
this.taskMetadata.priority =
priorityMap[metadata.priority] || 3;
}
if (tags && tags.length > 0) {
if (!this.taskMetadata.tags) {
this.taskMetadata.tags = [];
}
// Merge new tags with existing ones
tags.forEach((tag) => {
if (!this.taskMetadata.tags!.includes(tag)) {
this.taskMetadata.tags!.push(tag);
}
});
}
// Update button states
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
);
this.updateButtonState(
this.locationButton!,
!!(
this.taskMetadata.location ||
this.taskMetadata.targetFile ||
metadata.project ||
metadata.location
)
);
} catch (error) {
console.error("Error parsing content:", error);
// On error, still update button states
this.updateButtonState(
this.dateButton!,
!!this.taskMetadata.dueDate
);
this.updateButtonState(
this.priorityButton!,
!!this.taskMetadata.priority
);
this.updateButtonState(
this.tagButton!,
!!(this.taskMetadata.tags && this.taskMetadata.tags.length > 0)
);
this.updateButtonState(
this.locationButton!,
!!(this.taskMetadata.location || this.taskMetadata.targetFile)
);
}
}
/**
* Reset UI elements
*/
protected resetUIElements(): void {
// Reset button states
if (this.dateButton) this.updateButtonState(this.dateButton, false);
if (this.priorityButton)
this.updateButtonState(this.priorityButton, false);
if (this.tagButton) this.updateButtonState(this.tagButton, false);
if (this.locationButton)
this.updateButtonState(this.locationButton, false);
}
/**
* Clean up on close
*/
onClose() {
// Clean up universal suggest
if (this.universalSuggest) {
this.universalSuggest.disable();
this.universalSuggest = null;
}
// Clean up suggest
if (this.minimalSuggest) {
this.minimalSuggest.setMinimalMode(false);
}
// Clean up modal reference
delete (this.modalEl as any).__minimalQuickCaptureModal;
super.onClose();
}
}

View file

@ -0,0 +1,901 @@
import {
App,
Setting,
TFile,
Notice,
Platform,
MarkdownRenderer,
moment,
} from "obsidian";
import {
createEmbeddableMarkdownEditor,
EmbeddableMarkdownEditor,
} from "@/editor-extensions/core/markdown-editor";
import TaskProgressBarPlugin from "@/index";
import { FileSuggest } from "@/components/ui/inputs/AutoComplete";
import { t } from "@/translations/helper";
import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer";
import { StatusComponent } from "@/components/ui/feedback/StatusIndicator";
import { Task } from "@/types/task";
import {
ContextSuggest,
ProjectSuggest,
} from "@/components/ui/inputs/AutoComplete";
import {
TimeParsingService,
DEFAULT_TIME_PARSING_CONFIG,
LineParseResult,
} from "@/services/time-parsing-service";
import { UniversalEditorSuggest } from "@/components/ui/suggest";
import {
BaseQuickCaptureModal,
QuickCaptureMode,
TaskMetadata,
} from "./BaseQuickCaptureModal";
import { FileNameInput } from "../components/FileNameInput";
/**
* Enhanced Quick Capture Modal extending the base class
*/
export class QuickCaptureModal extends BaseQuickCaptureModal {
// Full mode specific elements
private previewContainerEl: HTMLElement | null = null;
private markdownRenderer: MarkdownRendererComponent | null = null;
private timeParsingService: TimeParsingService;
private universalSuggest: UniversalEditorSuggest | null = null;
// Date input references
private startDateInput?: HTMLInputElement;
private dueDateInput?: HTMLInputElement;
private scheduledDateInput?: HTMLInputElement;
// File name input for file creation mode
private fileNameInput: FileNameInput | null = null;
// UI element references
private targetSectionContainer: HTMLElement | null = null;
private configPanel: HTMLElement | null = null;
private metadataContainer: HTMLElement | null = null;
// Debounce timer for real-time parsing
private parseDebounceTimer?: number;
constructor(
app: App,
plugin: TaskProgressBarPlugin,
metadata?: TaskMetadata,
useFullFeaturedMode: boolean = false
) {
// Determine initial mode - default to checkbox (task) mode
let initialMode: QuickCaptureMode = "checkbox";
if (
plugin.settings.quickCapture.rememberLastMode &&
plugin.settings.quickCapture.lastUsedMode
) {
initialMode = plugin.settings.quickCapture.lastUsedMode;
}
super(app, plugin, initialMode, metadata);
// Initialize time parsing service
this.timeParsingService = new TimeParsingService(
this.plugin.settings.timeParsing || DEFAULT_TIME_PARSING_CONFIG
);
}
/**
* Initialize components after UI creation
*/
protected initializeComponents(): void {
// Setup markdown editor only if not already initialized
if (this.contentContainer && !this.markdownEditor) {
const editorContainer = this.contentContainer.querySelector(
".quick-capture-modal-editor"
) as HTMLElement;
if (editorContainer) {
this.setupMarkdownEditor(editorContainer);
}
// Enable universal suggest after editor is created
setTimeout(() => {
if (this.markdownEditor?.editor?.editor) {
this.universalSuggest =
this.suggestManager.enableForQuickCaptureModal(
this.markdownEditor.editor.editor
);
this.universalSuggest.enable();
}
}, 100);
}
// Restore content if switching modes
if (this.markdownEditor && this.capturedContent) {
this.markdownEditor.set(this.capturedContent, false);
}
}
/**
* Create UI - consistent layout for both modes
*/
protected createUI(): void {
if (!this.contentContainer) return;
// Create a layout container with two panels
const layoutContainer = this.contentContainer.createDiv({
cls: "quick-capture-layout",
});
// Create left panel for configuration
const configPanel = layoutContainer.createDiv({
cls: "quick-capture-config-panel",
});
// Create right panel for editor
const editorPanel = layoutContainer.createDiv({
cls: "quick-capture-editor-panel",
});
// Store config panel reference for updating
this.configPanel = configPanel;
// Create target section (will be updated based on mode)
this.createTargetSection(configPanel);
// Task metadata configuration (always shown)
this.metadataContainer = configPanel.createDiv({
cls: "quick-capture-metadata-container",
});
this.createTaskMetadataConfig(this.metadataContainer);
// Create editor in right panel
editorPanel.createDiv({
text:
this.currentMode === "file"
? t("File Content")
: t("Task Content"),
cls: "quick-capture-section-title",
});
const editorContainer = editorPanel.createDiv({
cls: "quick-capture-modal-editor",
});
// Preview container (only for checkbox mode)
if (this.currentMode === "checkbox") {
this.previewContainerEl = editorPanel.createDiv({
cls: "preview-container",
});
this.markdownRenderer = new MarkdownRendererComponent(
this.app,
this.previewContainerEl,
"",
false
);
}
}
/**
* Create target section that will be updated based on mode
*/
private createTargetSection(container: HTMLElement): void {
this.targetSectionContainer = container.createDiv({
cls: "quick-capture-target-container",
});
// Initial display based on current mode
this.updateTargetDisplay();
}
/**
* Update target display based on current mode
*/
protected updateTargetDisplay(): void {
if (!this.targetSectionContainer) return;
// Clear existing content
this.targetSectionContainer.empty();
if (this.currentMode === "checkbox") {
// Checkbox mode: "Capture to: [target file]"
this.createTargetFileSelector(this.targetSectionContainer);
} else {
// File mode: "Capture as: [file name input]"
this.createFileNameSelector(this.targetSectionContainer);
}
// Update metadata section visibility
if (this.metadataContainer) {
this.metadataContainer.style.display = "block";
if (this.metadataContainer.children.length === 0) {
this.createTaskMetadataConfig(this.metadataContainer);
}
}
// Update editor title
const editorTitle = this.contentContainer?.querySelector(
".quick-capture-section-title"
);
if (editorTitle) {
editorTitle.setText(
this.currentMode === "file"
? t("File Content")
: t("Task Content")
);
}
// Update preview visibility
if (this.previewContainerEl) {
this.previewContainerEl.style.display =
this.currentMode === "checkbox" ? "block" : "none";
}
}
/**
* Create file name selector for file mode
*/
private createFileNameSelector(container: HTMLElement): void {
const fileNameContainer = container;
fileNameContainer.createDiv({
text: t("Capture as:"),
cls: "quick-capture-section-title",
});
// Destroy previous file name input if exists
if (this.fileNameInput) {
this.fileNameInput.destroy();
this.fileNameInput = null;
}
// Create new file name input
this.fileNameInput = new FileNameInput(this.app, fileNameContainer, {
placeholder: t("Enter file name..."),
defaultValue:
this.plugin.settings.quickCapture.defaultFileNameTemplate ||
"{{DATE:YYYY-MM-DD}} - ",
currentFolder:
this.plugin.settings.quickCapture.createFileMode?.defaultFolder,
onChange: (value) => {
this.taskMetadata.customFileName = value;
},
});
// Set initial value if exists
if (this.taskMetadata.customFileName) {
this.fileNameInput.setValue(this.taskMetadata.customFileName);
}
}
/**
* Create target file selector
*/
private createTargetFileSelector(container: HTMLElement): void {
const targetFileContainer = container.createDiv({
cls: "quick-capture-target-container",
});
targetFileContainer.createDiv({
text: t("Capture to:"),
cls: "quick-capture-section-title",
});
const targetFileEl = targetFileContainer.createEl("div", {
cls: "quick-capture-target",
attr: {
contenteditable:
this.plugin.settings.quickCapture.targetType === "fixed"
? "true"
: "false",
spellcheck: "false",
},
text: this.tempTargetFilePath,
});
// Only add file suggest for fixed file type
if (this.plugin.settings.quickCapture.targetType === "fixed") {
new FileSuggest(
this.app,
targetFileEl,
this.plugin.settings.quickCapture,
(file: TFile) => {
targetFileEl.textContent = file.path;
this.tempTargetFilePath = file.path;
this.markdownEditor?.editor?.focus();
}
);
}
}
/**
* Create task metadata configuration
*/
private createTaskMetadataConfig(container: HTMLElement): void {
container.createDiv({
text: t("Task Properties"),
cls: "quick-capture-section-title",
});
// Status component
const statusComponent = new StatusComponent(
this.plugin,
container,
{
status: this.taskMetadata.status,
} as Task,
{
type: "quick-capture",
onTaskStatusSelected: (status: string) => {
this.taskMetadata.status = status;
this.updatePreview();
},
}
);
statusComponent.load();
// Date inputs
this.createDateInputs(container);
// Priority selector
this.createPrioritySelector(container);
// Project input
this.createProjectInput(container);
// Context input
this.createContextInput(container);
// Recurrence input
this.createRecurrenceInput(container);
}
/**
* Create date inputs
*/
private createDateInputs(container: HTMLElement): void {
// Start Date
new Setting(container).setName(t("Start Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.startDate
? this.formatDate(this.taskMetadata.startDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.startDate = this.parseDate(value);
this.markAsManuallySet("startDate");
} else {
this.taskMetadata.startDate = undefined;
if (this.taskMetadata.manuallySet) {
this.taskMetadata.manuallySet.startDate = false;
}
}
this.updatePreview();
});
text.inputEl.type = "date";
this.startDateInput = text.inputEl;
});
// Due Date
new Setting(container).setName(t("Due Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.dueDate
? this.formatDate(this.taskMetadata.dueDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.dueDate = this.parseDate(value);
this.markAsManuallySet("dueDate");
} else {
this.taskMetadata.dueDate = undefined;
if (this.taskMetadata.manuallySet) {
this.taskMetadata.manuallySet.dueDate = false;
}
}
this.updatePreview();
});
text.inputEl.type = "date";
this.dueDateInput = text.inputEl;
});
// Scheduled Date
new Setting(container).setName(t("Scheduled Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.scheduledDate
? this.formatDate(this.taskMetadata.scheduledDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.scheduledDate = this.parseDate(value);
this.markAsManuallySet("scheduledDate");
} else {
this.taskMetadata.scheduledDate = undefined;
if (this.taskMetadata.manuallySet) {
this.taskMetadata.manuallySet.scheduledDate = false;
}
}
this.updatePreview();
});
text.inputEl.type = "date";
this.scheduledDateInput = text.inputEl;
});
}
/**
* Create priority selector
*/
private createPrioritySelector(container: HTMLElement): void {
new Setting(container)
.setName(t("Priority"))
.addDropdown((dropdown) => {
dropdown
.addOption("", t("None"))
.addOption("5", t("Highest"))
.addOption("4", t("High"))
.addOption("3", t("Medium"))
.addOption("2", t("Low"))
.addOption("1", t("Lowest"))
.setValue(this.taskMetadata.priority?.toString() || "")
.onChange((value) => {
this.taskMetadata.priority = value
? parseInt(value)
: undefined;
this.updatePreview();
});
});
}
/**
* Create project input
*/
private createProjectInput(container: HTMLElement): void {
new Setting(container).setName(t("Project")).addText((text) => {
new ProjectSuggest(this.app, text.inputEl, this.plugin);
text.setPlaceholder(t("Project name"))
.setValue(this.taskMetadata.project || "")
.onChange((value) => {
this.taskMetadata.project = value || undefined;
this.updatePreview();
});
});
}
/**
* Create context input
*/
private createContextInput(container: HTMLElement): void {
new Setting(container).setName(t("Context")).addText((text) => {
new ContextSuggest(this.app, text.inputEl, this.plugin);
text.setPlaceholder(t("Context"))
.setValue(this.taskMetadata.context || "")
.onChange((value) => {
this.taskMetadata.context = value || undefined;
this.updatePreview();
});
});
}
/**
* Create recurrence input
*/
private createRecurrenceInput(container: HTMLElement): void {
new Setting(container).setName(t("Recurrence")).addText((text) => {
text.setPlaceholder(t("e.g., every day, every week"))
.setValue(this.taskMetadata.recurrence || "")
.onChange((value) => {
this.taskMetadata.recurrence = value || undefined;
this.updatePreview();
});
});
}
/**
* Setup markdown editor
*/
private setupMarkdownEditor(container: HTMLElement): void {
setTimeout(() => {
this.markdownEditor = createEmbeddableMarkdownEditor(
this.app,
container,
{
placeholder: this.plugin.settings.quickCapture.placeholder,
singleLine: this.currentMode === "checkbox",
onEnter: (editor, mod, shift) => {
if (mod) {
this.handleSubmit();
return true;
}
// In checkbox mode, Enter submits
if (this.currentMode === "checkbox") {
this.handleSubmit();
return true;
}
return false;
},
onEscape: (editor) => {
this.close();
},
onSubmit: (editor) => {
this.handleSubmit();
},
onChange: (update) => {
this.capturedContent = this.markdownEditor?.value || "";
// Clear previous debounce timer
if (this.parseDebounceTimer) {
clearTimeout(this.parseDebounceTimer);
}
// Debounce time parsing for checkbox mode
if (this.currentMode === "checkbox") {
this.parseDebounceTimer = window.setTimeout(() => {
this.performRealTimeParsing();
}, 300);
}
// Update preview for checkbox mode
if (this.currentMode === "checkbox") {
this.updatePreview();
}
},
}
);
// Focus the editor
this.markdownEditor?.editor?.focus();
// Restore content if exists
if (this.capturedContent && this.markdownEditor) {
this.markdownEditor.set(this.capturedContent, false);
}
}, 50);
}
/**
* Update preview
*/
private updatePreview(): void {
if (this.previewContainerEl && this.markdownRenderer) {
this.markdownRenderer.render(
this.processContentWithMetadata(this.capturedContent)
);
}
}
/**
* Process content with metadata
*/
protected processContentWithMetadata(content: string): string {
// For file mode, just return content as-is
if (this.currentMode === "file") {
return content;
}
// Split content into lines
const lines = content.split("\n");
const processedLines: string[] = [];
for (const line of lines) {
if (!line.trim()) {
processedLines.push(line);
continue;
}
// Parse time expressions for this line
const lineParseResult =
this.timeParsingService.parseTimeExpressionsForLine(line);
const cleanedLine = lineParseResult.cleanedLine;
// Check for indentation
const indentMatch = line.match(/^(\s+)/);
const isSubTask = indentMatch && indentMatch[1].length > 0;
// Check if line is already a task or list item
const isTaskOrList = cleanedLine
.trim()
.match(/^(-|\d+\.|\*|\+)(\s+\[[^\]\[]+\])?/);
if (isSubTask) {
// Don't add metadata to sub-tasks
const originalIndent = indentMatch[1];
processedLines.push(
originalIndent +
this.cleanTemporaryMarks(cleanedLine.trim())
);
} else if (isTaskOrList) {
// Process as task
if (cleanedLine.trim().match(/^(-|\d+\.|\*|\+)\s+\[[^\]]+\]/)) {
processedLines.push(
this.addLineMetadataToTask(cleanedLine, lineParseResult)
);
} else {
// Convert to task
const listPrefix = cleanedLine
.trim()
.match(/^(-|\d+\.|\*|\+)/)?.[0];
const restOfLine = this.cleanTemporaryMarks(
cleanedLine
.trim()
.substring(listPrefix?.length || 0)
.trim()
);
const statusMark = this.taskMetadata.status || " ";
const taskLine = `${listPrefix} [${statusMark}] ${restOfLine}`;
processedLines.push(
this.addLineMetadataToTask(taskLine, lineParseResult)
);
}
} else {
// Convert to task
const statusMark = this.taskMetadata.status || " ";
const cleanedContent = this.cleanTemporaryMarks(cleanedLine);
const taskLine = `- [${statusMark}] ${cleanedContent}`;
processedLines.push(
this.addLineMetadataToTask(taskLine, lineParseResult)
);
}
}
return processedLines.join("\n");
}
/**
* Add line metadata to task
*/
private addLineMetadataToTask(
taskLine: string,
lineParseResult: LineParseResult
): string {
const metadata = this.generateLineMetadata(lineParseResult);
if (!metadata) return taskLine;
return `${taskLine} ${metadata}`.trim();
}
/**
* Generate line metadata
*/
private generateLineMetadata(lineParseResult: LineParseResult): string {
const metadata: string[] = [];
const useDataviewFormat = this.preferMetadataFormat === "dataview";
// Use line-specific dates first, fall back to global metadata
const startDate =
lineParseResult.startDate || this.taskMetadata.startDate;
const dueDate = lineParseResult.dueDate || this.taskMetadata.dueDate;
const scheduledDate =
lineParseResult.scheduledDate || this.taskMetadata.scheduledDate;
// Add dates
if (startDate) {
const formattedDate = this.formatDate(startDate);
metadata.push(
useDataviewFormat
? `[start:: ${formattedDate}]`
: `🛫 ${formattedDate}`
);
}
if (dueDate) {
const formattedDate = this.formatDate(dueDate);
metadata.push(
useDataviewFormat
? `[due:: ${formattedDate}]`
: `📅 ${formattedDate}`
);
}
if (scheduledDate) {
const formattedDate = this.formatDate(scheduledDate);
metadata.push(
useDataviewFormat
? `[scheduled:: ${formattedDate}]`
: `${formattedDate}`
);
}
// Add priority
if (this.taskMetadata.priority) {
if (useDataviewFormat) {
const priorityMap: { [key: number]: string } = {
5: "highest",
4: "high",
3: "medium",
2: "low",
1: "lowest",
};
metadata.push(
`[priority:: ${priorityMap[this.taskMetadata.priority]}]`
);
} else {
const priorityIcons = ["⏬", "🔽", "🔼", "⏫", "🔺"];
metadata.push(priorityIcons[this.taskMetadata.priority - 1]);
}
}
// Add project
if (this.taskMetadata.project) {
const projectPrefix =
this.plugin.settings.projectTagPrefix?.[
this.plugin.settings.preferMetadataFormat
] || "project";
metadata.push(
useDataviewFormat
? `[${projectPrefix}:: ${this.taskMetadata.project}]`
: `#${projectPrefix}/${this.taskMetadata.project}`
);
}
// Add context
if (this.taskMetadata.context) {
const contextPrefix =
this.plugin.settings.contextTagPrefix?.[
this.plugin.settings.preferMetadataFormat
] || "@";
metadata.push(
useDataviewFormat
? `[context:: ${this.taskMetadata.context}]`
: `${contextPrefix}${this.taskMetadata.context}`
);
}
// Add recurrence
if (this.taskMetadata.recurrence) {
metadata.push(
useDataviewFormat
? `[repeat:: ${this.taskMetadata.recurrence}]`
: `🔁 ${this.taskMetadata.recurrence}`
);
}
return metadata.join(" ");
}
/**
* Clean temporary marks
*/
private cleanTemporaryMarks(content: string): string {
let cleaned = content;
cleaned = cleaned.replace(/\s*!\s*/g, " ");
cleaned = cleaned.replace(/\s*~\s*/g, " ");
cleaned = cleaned.replace(/\s*[🔺⏫🔼🔽⏬️]\s*/g, " ");
cleaned = cleaned.replace(/\s*[📅🛫⏳✅➕❌]\s*/g, " ");
cleaned = cleaned.replace(/\s*[📁🏠🏢🏪🏫🏬🏭🏯🏰]\s*/g, " ");
cleaned = cleaned.replace(/\s*[🆔⛔🏁🔁]\s*/g, " ");
cleaned = cleaned.replace(/\s*@\w*\s*/g, " ");
cleaned = cleaned.replace(/\s*target:\s*/gi, " ");
cleaned = cleaned.replace(/\s+/g, " ").trim();
return cleaned;
}
/**
* Perform real-time parsing
*/
private performRealTimeParsing(): void {
if (!this.capturedContent) return;
const lines = this.capturedContent.split("\n");
const lineParseResults =
this.timeParsingService.parseTimeExpressionsPerLine(lines);
// Aggregate dates from all lines
let aggregatedStartDate: Date | undefined;
let aggregatedDueDate: Date | undefined;
let aggregatedScheduledDate: Date | undefined;
for (const lineResult of lineParseResults) {
if (lineResult.startDate && !aggregatedStartDate) {
aggregatedStartDate = lineResult.startDate;
}
if (lineResult.dueDate && !aggregatedDueDate) {
aggregatedDueDate = lineResult.dueDate;
}
if (lineResult.scheduledDate && !aggregatedScheduledDate) {
aggregatedScheduledDate = lineResult.scheduledDate;
}
}
// Update metadata (only if not manually set)
if (aggregatedStartDate && !this.isManuallySet("startDate")) {
this.taskMetadata.startDate = aggregatedStartDate;
if (this.startDateInput) {
this.startDateInput.value =
this.formatDate(aggregatedStartDate);
}
}
if (aggregatedDueDate && !this.isManuallySet("dueDate")) {
this.taskMetadata.dueDate = aggregatedDueDate;
if (this.dueDateInput) {
this.dueDateInput.value = this.formatDate(aggregatedDueDate);
}
}
if (aggregatedScheduledDate && !this.isManuallySet("scheduledDate")) {
this.taskMetadata.scheduledDate = aggregatedScheduledDate;
if (this.scheduledDateInput) {
this.scheduledDateInput.value = this.formatDate(
aggregatedScheduledDate
);
}
}
}
/**
* Check if metadata field was manually set
*/
private isManuallySet(
field: "startDate" | "dueDate" | "scheduledDate"
): boolean {
return this.taskMetadata.manuallySet?.[field] || false;
}
/**
* Mark metadata field as manually set
*/
private markAsManuallySet(
field: "startDate" | "dueDate" | "scheduledDate"
): void {
if (!this.taskMetadata.manuallySet) {
this.taskMetadata.manuallySet = {};
}
this.taskMetadata.manuallySet[field] = true;
}
/**
* Reset UI elements
*/
protected resetUIElements(): void {
// Reset date inputs
if (this.startDateInput) this.startDateInput.value = "";
if (this.dueDateInput) this.dueDateInput.value = "";
if (this.scheduledDateInput) this.scheduledDateInput.value = "";
// Clear file name input
if (this.fileNameInput) {
this.fileNameInput.clear();
}
// Clear preview
if (this.previewContainerEl) {
this.previewContainerEl.empty();
}
}
/**
* Called when modal is closed
*/
onClose() {
// Save last used mode if enabled
if (this.plugin.settings.quickCapture.rememberLastMode) {
this.plugin.settings.quickCapture.lastUsedMode = this.currentMode;
this.plugin.saveSettings();
}
// Clean up
if (this.universalSuggest) {
this.universalSuggest.disable();
this.universalSuggest = null;
}
if (this.parseDebounceTimer) {
clearTimeout(this.parseDebounceTimer);
}
if (this.markdownRenderer) {
this.markdownRenderer.unload();
this.markdownRenderer = null;
}
if (this.fileNameInput) {
this.fileNameInput.destroy();
this.fileNameInput = null;
}
super.onClose();
}
}

View file

@ -1,4 +1,4 @@
import { Setting, Notice, App } from "obsidian";
import { Setting, Notice } from "obsidian";
import { TaskProgressBarSettingTab } from "@/setting";
import { t } from "@/translations/helper";
@ -219,15 +219,18 @@ export function renderQuickCaptureSettingsTab(
settingTab.applySettingsUpdate();
})
);
// Task prefix setting
new Setting(containerEl)
.setName(t("Auto-add task prefix"))
.setDesc(t("Automatically add task checkbox prefix to captured content"))
.setDesc(
t("Automatically add task checkbox prefix to captured content")
)
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.quickCapture.autoAddTaskPrefix ?? true
settingTab.plugin.settings.quickCapture.autoAddTaskPrefix ??
true
)
.onChange(async (value) => {
settingTab.plugin.settings.quickCapture.autoAddTaskPrefix =
@ -239,16 +242,21 @@ export function renderQuickCaptureSettingsTab(
}, 100);
})
);
// Custom task prefix
if (settingTab.plugin.settings.quickCapture.autoAddTaskPrefix) {
new Setting(containerEl)
.setName(t("Task prefix format"))
.setDesc(t("The prefix to add before captured content (e.g., '- [ ]' for task, '- ' for list item)"))
.setDesc(
t(
"The prefix to add before captured content (e.g., '- [ ]' for task, '- ' for list item)"
)
)
.addText((text) =>
text
.setValue(
settingTab.plugin.settings.quickCapture.taskPrefix || "- [ ]"
settingTab.plugin.settings.quickCapture.taskPrefix ||
"- [ ]"
)
.onChange(async (value) => {
settingTab.plugin.settings.quickCapture.taskPrefix =
@ -258,6 +266,127 @@ export function renderQuickCaptureSettingsTab(
);
}
new Setting(containerEl).setName(t("Enhanced")).setHeading();
// Keep open after capture
new Setting(containerEl)
.setName(t("Keep open after capture"))
.setDesc(t("Keep the modal open after capturing content"))
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.quickCapture
.keepOpenAfterCapture || false
)
.onChange(async (value) => {
settingTab.plugin.settings.quickCapture.keepOpenAfterCapture =
value;
settingTab.applySettingsUpdate();
})
);
// Remember last mode
new Setting(containerEl)
.setName(t("Remember last mode"))
.setDesc(t("Remember the last used quick capture mode"))
.addToggle((toggle) =>
toggle
.setValue(
settingTab.plugin.settings.quickCapture.rememberLastMode ??
true
)
.onChange(async (value) => {
settingTab.plugin.settings.quickCapture.rememberLastMode =
value;
settingTab.applySettingsUpdate();
})
);
// File creation mode settings
new Setting(containerEl).setName(t("File Creation Mode")).setHeading();
// Initialize createFileMode if not exists and keep a local reference for type safety
const createFileMode =
(settingTab.plugin.settings.quickCapture.createFileMode ||= {
defaultFolder: "",
useTemplate: false,
templateFile: "",
});
// Default folder for file creation
new Setting(containerEl)
.setName(t("Default folder for new files"))
.setDesc(
t(
"Used by File mode (requires FileSource). Leave empty for vault root."
)
)
.addText((text) =>
text
.setValue(createFileMode.defaultFolder || "")
.onChange(async (value) => {
createFileMode.defaultFolder = value;
settingTab.applySettingsUpdate();
})
);
// Use template for new files
new Setting(containerEl)
.setName(t("Use template for new files"))
.setDesc(
t(
"When File mode is used, ensure the new file has frontmatter; if enabled, only frontmatter is auto-inserted when missing."
)
)
.addToggle((toggle) =>
toggle
.setValue(createFileMode.useTemplate || false)
.onChange(async (value) => {
createFileMode.useTemplate = value;
settingTab.applySettingsUpdate();
// Refresh to show/hide template field
setTimeout(() => {
settingTab.display();
}, 100);
})
);
// Default file name template (File mode)
new Setting(containerEl)
.setName(t("Default file name template"))
.setDesc(
t(
"Used by File mode to prefill the file name input (supports date templates like {{DATE:YYYY-MM-DD}})"
)
)
.addText((text) =>
text
.setValue(
settingTab.plugin.settings.quickCapture
.defaultFileNameTemplate || "{{DATE:YYYY-MM-DD}} - "
)
.onChange(async (value) => {
settingTab.plugin.settings.quickCapture.defaultFileNameTemplate =
value;
settingTab.applySettingsUpdate();
})
);
// Template file path
if (createFileMode.useTemplate) {
new Setting(containerEl)
.setName(t("Template file"))
.setDesc(t("Template file to use for new files"))
.addText((text) =>
text
.setValue(createFileMode.templateFile || "")
.onChange(async (value) => {
createFileMode.templateFile = value;
settingTab.applySettingsUpdate();
})
);
}
// Minimal mode settings
new Setting(containerEl).setName(t("Minimal Mode")).setHeading();

View file

@ -91,7 +91,7 @@ export interface QuickCaptureOptions {
placeholder?: string;
appendToFile?: "append" | "prepend" | "replace";
// New options for enhanced quick capture
targetType?: "fixed" | "daily-note";
targetType?: "fixed" | "daily-note" | "custom-file";
targetHeading?: string;
dailyNoteSettings?: {
format: string;

View file

@ -65,8 +65,9 @@ import {
migrateOldFilterOptions,
} from "./editor-extensions/core/task-filter-panel";
import { Task } from "./types/task";
import { QuickCaptureModal } from "./components/features/quick-capture/modals/QuickCaptureModal";
import { MinimalQuickCaptureModal } from "./components/features/quick-capture/modals/MinimalQuickCaptureModal";
// Import the enhanced QuickCaptureModal and MinimalQuickCaptureModal
import { QuickCaptureModal } from "./components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
import { MinimalQuickCaptureModal } from "./components/features/quick-capture/modals/MinimalQuickCaptureModalWithSwitch";
import { MinimalQuickCaptureSuggest } from "./components/features/quick-capture/suggest/MinimalQuickCaptureSuggest";
import { SuggestManager } from "./components/ui/suggest";
import { MarkdownView } from "obsidian";
@ -705,7 +706,8 @@ export default class TaskProgressBarPlugin extends Plugin {
name: t("Quick Capture"),
callback: () => {
// Create a modal with full task metadata options
new QuickCaptureModal(this.app, this, {}, true).open();
// The new modal will automatically handle mode switching
new QuickCaptureModal(this.app, this, undefined, true).open();
},
});
@ -719,6 +721,20 @@ export default class TaskProgressBarPlugin extends Plugin {
},
});
// Add command for quick file creation
this.addCommand({
id: "quick-file-create",
name: t("Quick File Create"),
callback: () => {
// Create a modal with file creation mode metadata
const modal = new QuickCaptureModal(this.app, this, {
location: "file",
});
// The modal will detect file location and switch to file mode
modal.open();
},
});
// Add command for toggling task filter
this.addCommand({
id: "toggle-task-filter",

View file

@ -1,3 +1,223 @@
/* Quick Capture Enhanced Styles - with shadcn theme */
/* shadcn-inspired theme colors integrated with Task Genius */
:root {
/* Tab component colors */
--tg-tab-background: 0 0% 100%;
--tg-tab-foreground: 240 10% 3.9%;
--tg-tab-muted: 240 4.8% 95.9%;
--tg-tab-muted-foreground: 240 3.8% 46.1%;
--tg-tab-border: 240 5.9% 90%;
}
.theme-dark {
--tg-tab-background: 240 10% 3.9%;
--tg-tab-foreground: 0 0% 98%;
--tg-tab-muted: 240 3.7% 15.9%;
--tg-tab-muted-foreground: 240 5% 64.9%;
--tg-tab-border: 240 3.7% 15.9%;
}
/* Quick Capture Header - shadcn style */
.quick-capture-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid hsl(var(--tg-tab-border));
background: hsl(var(--tg-tab-background));
}
/* Quick Capture Tabs - shadcn style */
.quick-capture-tabs {
display: inline-flex;
padding: 4px;
background: hsl(var(--tg-tab-muted));
border-radius: 6px;
gap: 2px;
}
.quick-capture-tab {
padding: 6px 12px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
color: hsl(var(--tg-tab-muted-foreground));
font-size: 14px;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
position: relative;
}
.quick-capture-tab:hover:not(.active) {
color: hsl(var(--tg-tab-foreground));
}
.quick-capture-tab.active {
background: hsl(var(--tg-tab-background));
color: hsl(var(--tg-tab-foreground));
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
/* Quick Capture Tab Button Icon and Text - shadcn style */
.quick-capture-tab-icon {
margin-right: 6px;
display: inline-flex;
align-items: center;
width: 16px;
height: 16px;
}
.quick-capture-tab-icon svg {
width: 14px;
height: 14px;
}
.quick-capture-tab-text {
display: inline-flex;
align-items: center;
line-height: 1;
}
/* Quick Capture Clear Button - shadcn style */
.quick-capture-clear {
padding: 6px 12px;
background: transparent;
border: 1px solid hsl(var(--tg-tab-border));
border-radius: 6px;
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
color: hsl(var(--tg-tab-muted-foreground));
font-size: 14px;
font-weight: 500;
}
.quick-capture-clear:hover {
background: hsl(var(--tg-tab-muted));
color: hsl(var(--tg-tab-foreground));
border-color: hsl(var(--tg-tab-muted-foreground));
}
/* Quick Capture Content */
.quick-capture-content {
flex: 1;
overflow-y: auto;
padding: var(--size-4-3);
}
/* Quick Capture Footer */
.quick-capture-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--size-4-2);
border-top: 1px solid var(--background-modifier-border);
}
.quick-capture-footer-left,
.quick-capture-footer-right {
display: flex;
gap: var(--size-2-2);
}
.quick-capture-continue {
padding: var(--size-2-2) var(--size-4-3);
background: transparent;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-muted);
}
.quick-capture-continue:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
border-color: var(--interactive-accent);
}
/* File Name Input Component */
.file-name-input-container {
display: flex;
flex-direction: column;
gap: var(--size-2-1);
margin-bottom: var(--size-4-3);
}
.file-name-label {
font-size: var(--font-ui-small);
color: var(--text-muted);
font-weight: 500;
}
.file-name-input {
width: 100%;
padding: var(--size-2-2);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background-color: var(--background-primary);
color: var(--text-normal);
}
.file-name-input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--interactive-accent-hover);
}
.file-name-templates {
margin-top: var(--size-4-2);
}
.templates-label {
font-size: var(--font-ui-small);
color: var(--text-muted);
margin-bottom: var(--size-2-1);
}
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--size-2-1);
}
.template-button {
padding: var(--size-2-1) var(--size-2-3);
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
cursor: pointer;
transition: all 0.2s ease;
font-size: var(--font-ui-smaller);
color: var(--text-muted);
}
.template-button:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
border-color: var(--interactive-accent);
}
/* Mode-specific adjustments */
.quick-capture-modal.quick-capture-checkbox {
width: 800px;
max-width: 90vw;
height: 600px;
max-height: 80vh;
}
.quick-capture-modal.quick-capture-file {
width: 700px;
max-width: 90vw;
height: 500px;
max-height: 70vh;
}
/* Quick Capture Panel */
.quick-capture-panel {
padding: var(--size-4-2);
@ -43,34 +263,52 @@
.quick-capture-modal.minimal {
max-width: 600px;
min-width: 500px;
max-height: 200px;
max-height: 300px;
}
.quick-capture-minimal-editor-container {
padding: var(--size-4-2);
min-height: 50px;
/* Minimal Quick Capture Target Display */
.quick-capture-minimal-target-container {
padding: 8px 12px;
background: hsl(var(--tg-tab-muted));
border-radius: 6px;
margin-bottom: 12px;
}
.quick-capture-minimal-editor-container .cm-editor {
font-size: var(--font-text-size);
min-height: 40px;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-2-1);
}
.quick-capture-minimal-editor-container .cm-editor.cm-focused {
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--interactive-accent-alpha);
}
.quick-capture-minimal-buttons {
.quick-capture-minimal-target {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--size-4-2);
gap: 8px;
font-size: 14px;
}
.quick-capture-target-label {
color: hsl(var(--tg-tab-muted-foreground));
font-weight: 500;
}
.quick-capture-target-value {
color: hsl(var(--tg-tab-foreground));
flex: 1;
}
.quick-capture-minimal-file-input {
flex: 1;
padding: 4px 8px;
background: hsl(var(--tg-tab-background));
border: 1px solid hsl(var(--tg-tab-border));
border-radius: 4px;
font-size: 14px;
color: hsl(var(--tg-tab-foreground));
}
.quick-capture-minimal-file-input:focus {
outline: none;
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--interactive-accent-hover);
}
/* Removed minimal mode styles as minimal UI mode no longer exists */
.quick-actions-left {
display: flex;
gap: var(--size-2-1);

File diff suppressed because one or more lines are too long