mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
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:
parent
e18ea53b35
commit
09a2e0f2ea
12 changed files with 3231 additions and 110 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
22
src/index.ts
22
src/index.ts
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
231
styles.css
231
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue