refactor(quick-capture): migrate to QuickCaptureModalWithSwitch and improve UI

- Update all imports from QuickCaptureModal to QuickCaptureModalWithSwitch
- Conditionally show clear button only when both file and inline modes are available
- Hide tab container when only one mode is available (aria-hidden attribute)
- Adjust modal height from 750px to 700px for better viewport fit
- Add CSS rule to hide modal title when containing hidden elements
- Clean up formatting and spacing consistency
This commit is contained in:
Quorafind 2025-10-10 09:12:34 +08:00
parent 0ae3957144
commit ab8af59b34
8 changed files with 134 additions and 119 deletions

View file

@ -22,7 +22,7 @@ import { DayView } from "./views/day-view";
import { AgendaView } from "./views/agenda-view";
import { YearView } from "./views/year-view";
import TaskProgressBarPlugin from "@/index";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
// Import algorithm functions (optional for now, could be used within views)
// import { calculateEventLayout, determineEventColor } from './algorithm';

View file

@ -2,7 +2,7 @@ import { App, Component, setIcon } from "obsidian";
import { Task } from "@/types/task"; // Adjust path
import { KanbanCardComponent } from "./kanban-card";
import TaskProgressBarPlugin from "@/index"; // Adjust path
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal"; // Import QuickCaptureModal
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch"; // Import QuickCaptureModal
import { t } from "@/translations/helper"; // Import translation helper
const BATCH_SIZE = 20; // Number of cards to load at a time
@ -45,7 +45,7 @@ export class KanbanColumnComponent extends Component {
override onload(): void {
this.element = this.containerEl.createDiv({
cls: "tg-kanban-column",
attr: { "data-status-name": this.statusName },
attr: {"data-status-name": this.statusName},
});
// Hide column if no tasks and hideEmptyColumns is enabled
@ -122,7 +122,7 @@ export class KanbanColumnComponent extends Component {
new QuickCaptureModal(
this.app,
this.plugin,
{ status: taskStatusSymbol },
{status: taskStatusSymbol},
true
).open();
});

View file

@ -1,7 +1,7 @@
import { Setting, TextAreaComponent, Notice, setIcon } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import { t } from "@/translations/helper";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
export class TaskCreationGuide {
private plugin: TaskProgressBarPlugin;
@ -37,18 +37,18 @@ export class TaskCreationGuide {
*/
private renderTaskFormats(containerEl: HTMLElement) {
const formatsSection = containerEl.createDiv("task-formats-section");
formatsSection.createEl("h3", { text: t("Task Format Examples") });
formatsSection.createEl("h3", {text: t("Task Format Examples")});
// Basic task format
const basicFormat = formatsSection.createDiv("format-example");
basicFormat.createEl("h4", { text: t("Basic Task") });
basicFormat.createEl("h4", {text: t("Basic Task")});
basicFormat.createEl("code", {
text: "- [ ] Complete project documentation",
});
// Emoji format
const emojiFormat = formatsSection.createDiv("format-example");
emojiFormat.createEl("h4", { text: t("With Emoji Metadata") });
emojiFormat.createEl("h4", {text: t("With Emoji Metadata")});
emojiFormat.createEl("code", {
text: "- [ ] Complete project documentation 📅 2024-01-15 🔺 #project/docs",
});
@ -62,14 +62,14 @@ export class TaskCreationGuide {
// Dataview format
const dataviewFormat = formatsSection.createDiv("format-example");
dataviewFormat.createEl("h4", { text: t("With Dataview Metadata") });
dataviewFormat.createEl("h4", {text: t("With Dataview Metadata")});
dataviewFormat.createEl("code", {
text: "- [ ] Complete project documentation [due:: 2024-01-15] [priority:: high] [project:: docs]",
});
// Mixed format
const mixedFormat = formatsSection.createDiv("format-example");
mixedFormat.createEl("h4", { text: t("Mixed Format") });
mixedFormat.createEl("h4", {text: t("Mixed Format")});
mixedFormat.createEl("code", {
text: "- [ ] Complete project documentation 📅 2024-01-15 [priority:: high] @work",
});
@ -81,42 +81,42 @@ export class TaskCreationGuide {
// Status markers
const statusSection = formatsSection.createDiv("status-markers");
statusSection.createEl("h4", { text: t("Task Status Markers") });
statusSection.createEl("h4", {text: t("Task Status Markers")});
const statusList = statusSection.createEl("ul", { cls: "status-list" });
const statusList = statusSection.createEl("ul", {cls: "status-list"});
const statusMarkers = [
{ marker: "[ ]", description: t("Not started") },
{ marker: "[x]", description: t("Completed") },
{ marker: "[/]", description: t("In progress") },
{ marker: "[?]", description: t("Planned") },
{ marker: "[-]", description: t("Abandoned") },
{marker: "[ ]", description: t("Not started")},
{marker: "[x]", description: t("Completed")},
{marker: "[/]", description: t("In progress")},
{marker: "[?]", description: t("Planned")},
{marker: "[-]", description: t("Abandoned")},
];
statusMarkers.forEach((status) => {
const item = statusList.createEl("li");
item.createEl("code", { text: status.marker });
item.createEl("code", {text: status.marker});
item.createSpan().setText(" - " + status.description);
});
// Metadata symbols
const metadataSection = formatsSection.createDiv("metadata-symbols");
metadataSection.createEl("h4", { text: t("Common Metadata Symbols") });
metadataSection.createEl("h4", {text: t("Common Metadata Symbols")});
const symbolsList = metadataSection.createEl("ul", {
cls: "symbols-list",
});
const symbols = [
{ symbol: "📅", description: t("Due date") },
{ symbol: "🛫", description: t("Start date") },
{ symbol: "⏳", description: t("Scheduled date") },
{ symbol: "🔺", description: t("High priority") },
{ symbol: "⏫", description: t("Higher priority") },
{ symbol: "🔼", description: t("Medium priority") },
{ symbol: "🔽", description: t("Lower priority") },
{ symbol: "⏬", description: t("Lowest priority") },
{ symbol: "🔁", description: t("Recurring task") },
{ symbol: "#", description: t("Project/tag") },
{ symbol: "@", description: t("Context") },
{symbol: "📅", description: t("Due date")},
{symbol: "🛫", description: t("Start date")},
{symbol: "⏳", description: t("Scheduled date")},
{symbol: "🔺", description: t("High priority")},
{symbol: "⏫", description: t("Higher priority")},
{symbol: "🔼", description: t("Medium priority")},
{symbol: "🔽", description: t("Lower priority")},
{symbol: "⏬", description: t("Lowest priority")},
{symbol: "🔁", description: t("Recurring task")},
{symbol: "#", description: t("Project/tag")},
{symbol: "@", description: t("Context")},
];
symbols.forEach((symbol) => {
@ -134,7 +134,7 @@ export class TaskCreationGuide {
const quickCaptureSection = containerEl.createDiv(
"quick-capture-section"
);
quickCaptureSection.createEl("h3", { text: t("Quick Capture") });
quickCaptureSection.createEl("h3", {text: t("Quick Capture")});
const demoContent = quickCaptureSection.createDiv("demo-content");
demoContent.createEl("p", {
@ -179,7 +179,7 @@ export class TaskCreationGuide {
*/
private renderInteractivePractice(containerEl: HTMLElement) {
const practiceSection = containerEl.createDiv("practice-section");
practiceSection.createEl("h3", { text: t("Try It Yourself") });
practiceSection.createEl("h3", {text: t("Try It Yourself")});
practiceSection.createEl("p", {
text: t("Practice creating a task with the format you prefer:"),

View file

@ -50,6 +50,7 @@ export interface TaskMetadata {
* Provides shared functionality and state management
*/
const LAST_USED_MODE_KEY = "task-genius.lastUsedQuickCaptureMode";
export abstract class BaseQuickCaptureModal extends Modal {
plugin: TaskProgressBarPlugin;
protected markdownEditor: EmbeddableMarkdownEditor | null = null;
@ -154,7 +155,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
* Called when the modal is opened
*/
onOpen() {
const { contentEl } = this;
const {contentEl} = this;
this.modalEl.toggleClass("quick-capture-modal", true);
this.modalEl.toggleClass(`quick-capture-${this.currentMode}`, true);
@ -277,18 +278,21 @@ export abstract class BaseQuickCaptureModal extends Modal {
if (!(this.fileModeAvailable && this.inlineModeAvailable)) {
tabContainer.classList.add("is-hidden");
tabContainer.setAttribute("aria-hidden", "true");
}
// 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());
if (this.fileModeAvailable && this.inlineModeAvailable) {
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());
}
}
/**
@ -350,12 +354,15 @@ export abstract class BaseQuickCaptureModal extends Modal {
// Save current state
const savedContent = this.capturedContent;
const savedMetadata = { ...this.taskMetadata };
const savedMetadata = {...this.taskMetadata};
// Update mode
this.currentMode = mode;
// Persist last used mode to local storage
try { this.app.saveLocalStorage(LAST_USED_MODE_KEY, mode); } catch {}
try {
this.app.saveLocalStorage(LAST_USED_MODE_KEY, mode);
} catch {
}
// Update modal classes
this.modalEl.removeClass(
@ -632,44 +639,44 @@ export abstract class BaseQuickCaptureModal extends Modal {
* Sanitize filename
*/
/**
* Map UI status (symbol or text) to textual metadata
*/
protected mapStatusToText(status?: string): string {
if (!status) return "not-started";
if (status.length > 1) return status; // already textual
switch (status) {
case "x":
case "X":
return "completed";
case "/":
case ">":
return "in-progress";
case "?":
return "planned";
case "-":
return "cancelled";
case " ":
default:
return "not-started";
}
/**
* Map UI status (symbol or text) to textual metadata
*/
protected mapStatusToText(status?: string): string {
if (!status) return "not-started";
if (status.length > 1) return status; // already textual
switch (status) {
case "x":
case "X":
return "completed";
case "/":
case ">":
return "in-progress";
case "?":
return "planned";
case "-":
return "cancelled";
case " ":
default:
return "not-started";
}
}
/**
* Extract #tags from content for frontmatter tags array
* Simple regex scan; remove leading '#', dedupe
*/
protected extractTagsFromContentForFrontmatter(content: string): string[] {
if (!content) return [];
const tagRegex = /(^|\s)#([A-Za-z0-9_\/-]+)/g;
const results = new Set<string>();
let match: RegExpExecArray | null;
while ((match = tagRegex.exec(content)) !== null) {
const tag = match[2];
if (tag) results.add(tag);
}
return Array.from(results);
/**
* Extract #tags from content for frontmatter tags array
* Simple regex scan; remove leading '#', dedupe
*/
protected extractTagsFromContentForFrontmatter(content: string): string[] {
if (!content) return [];
const tagRegex = /(^|\s)#([A-Za-z0-9_\/-]+)/g;
const results = new Set<string>();
let match: RegExpExecArray | null;
while ((match = tagRegex.exec(content)) !== null) {
const tag = match[2];
if (tag) results.add(tag);
}
return Array.from(results);
}
protected sanitizeFilename(filename: string): string {
return filename

View file

@ -13,7 +13,7 @@ import { Task } from "@/types/task";
import { TimeComponent } from "@/types/time-parsing";
import { t } from "@/translations/helper";
import TaskProgressBarPlugin from "@/index";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
import {
createEmbeddableMarkdownEditor,
EmbeddableMarkdownEditor,
@ -219,7 +219,7 @@ export class TimelineSidebarView extends ItemView {
// Add collapse button to header
const headerLeft = this.quickInputHeaderEl.createDiv("quick-input-header-left");
const collapseBtn = headerLeft.createDiv("quick-input-collapse-btn");
setIcon(collapseBtn, "chevron-down");
collapseBtn.setAttribute("aria-label", t("Collapse quick input"));
@ -303,7 +303,7 @@ export class TimelineSidebarView extends ItemView {
private async loadEvents(): Promise<void> {
// Get tasks from the plugin's dataflow
let allTasks: Task[] = [];
if (this.plugin.dataflowOrchestrator) {
try {
allTasks = await this.plugin.dataflowOrchestrator.getQueryAPI().getAllTasks();
@ -317,10 +317,10 @@ export class TimelineSidebarView extends ItemView {
// Filter tasks based on showCompletedTasks setting
const shouldShowCompletedTasks =
this.plugin.settings.timelineSidebar.showCompletedTasks;
// Get abandoned status markers to filter out
const abandonedStatuses = this.plugin.settings.taskStatuses.abandoned.split("|");
const filteredTasks = shouldShowCompletedTasks
? allTasks
: allTasks.filter((task) => {
@ -343,7 +343,7 @@ export class TimelineSidebarView extends ItemView {
// Convert tasks to timeline events
timelineFilteredTasks.forEach((task) => {
const dates = this.extractDatesFromTask(task);
dates.forEach(({ date, type }) => {
dates.forEach(({date, type}) => {
const event: EnhancedTimelineEvent = {
id: `${task.id}-${type}`,
content: task.content,
@ -402,11 +402,11 @@ export class TimelineSidebarView extends ItemView {
const currentPriority =
DATE_TYPE_PRIORITY[
current.type as keyof typeof DATE_TYPE_PRIORITY
] || 0;
] || 0;
const highestPriority =
DATE_TYPE_PRIORITY[
highest.type as keyof typeof DATE_TYPE_PRIORITY
] || 0;
] || 0;
return currentPriority > highestPriority
? current
@ -502,7 +502,7 @@ export class TimelineSidebarView extends ItemView {
// Determine if this is a time range
// Check if the time component is marked as a range OR if we have an explicit end time
const isRange = relevantTimeComponent.isRange || !!relevantEndTime;
// If the time component is a range but we don't have enhancedDates.endDateTime,
// create the end time from the range partner
if (relevantTimeComponent.isRange && !relevantEndTime && relevantTimeComponent.rangePartner) {
@ -530,30 +530,30 @@ export class TimelineSidebarView extends ItemView {
task: Task
): Array<{ date: Date; type: string }> {
// Task-level deduplication: ensure each task appears only once in timeline
// Check if task has enhanced metadata with time components
const enhancedMetadata = task.metadata as any;
const timeComponents = enhancedMetadata?.timeComponents;
const enhancedDates = enhancedMetadata?.enhancedDates;
// For completed tasks: prioritize due date, fallback to completed date
if (task.completed) {
if (task.metadata.dueDate) {
// Use enhanced due datetime if available, otherwise use original timestamp
const dueDate = enhancedDates?.dueDateTime || new Date(task.metadata.dueDate);
return [{ date: dueDate, type: "due" }];
return [{date: dueDate, type: "due"}];
} else if (task.metadata.completedDate) {
return [{ date: new Date(task.metadata.completedDate), type: "completed" }];
return [{date: new Date(task.metadata.completedDate), type: "completed"}];
}
}
// For non-completed tasks: select single highest priority date with enhanced datetime support
const dates: Array<{ date: Date; type: string }> = [];
if (task.metadata.dueDate) {
// Use enhanced due datetime if available
const dueDate = enhancedDates?.dueDateTime || new Date(task.metadata.dueDate);
dates.push({ date: dueDate, type: "due" });
dates.push({date: dueDate, type: "due"});
}
if (task.metadata.scheduledDate) {
// Use enhanced scheduled datetime if available
@ -571,7 +571,7 @@ export class TimelineSidebarView extends ItemView {
type: "start",
});
}
// For non-completed tasks, select the highest priority date
if (dates.length > 0) {
const highestPriorityDate = dates.reduce((highest, current) => {
@ -581,7 +581,7 @@ export class TimelineSidebarView extends ItemView {
});
return [highestPriorityDate];
}
// Fallback: if no planning dates exist, use deduplication for edge cases
const allDates: Array<{ date: Date; type: string }> = [];
if (task.metadata.completedDate) {
@ -590,7 +590,7 @@ export class TimelineSidebarView extends ItemView {
type: "completed",
});
}
return this.deduplicateDatesByPriority(allDates);
}
@ -679,7 +679,7 @@ export class TimelineSidebarView extends ItemView {
private renderEventTime(timeEl: HTMLElement, event: EnhancedTimelineEvent): void {
if (event.timeInfo?.timeComponent) {
// Use parsed time component for accurate display
const { timeComponent, isRange, endTime } = event.timeInfo;
const {timeComponent, isRange, endTime} = event.timeInfo;
if (isRange && endTime) {
// Display time range
@ -697,7 +697,8 @@ export class TimelineSidebarView extends ItemView {
: `${minutes}m`;
timeEl.setAttribute("data-duration", duration);
}
} catch (_) {}
} catch (_) {
}
} else {
// Display single time
timeEl.setText(this.formatTimeComponent(timeComponent));
@ -753,12 +754,12 @@ export class TimelineSidebarView extends ItemView {
private formatTimeComponent(timeComponent: TimeComponent): string {
const hour = timeComponent.hour.toString().padStart(2, '0');
const minute = timeComponent.minute.toString().padStart(2, '0');
if (timeComponent.second !== undefined) {
const second = timeComponent.second.toString().padStart(2, '0');
return `${hour}:${minute}:${second}`;
}
return `${hour}:${minute}`;
}
@ -770,14 +771,14 @@ export class TimelineSidebarView extends ItemView {
// Get the primary time for sorting - use enhanced time if available
const timeA = a.timeInfo?.primaryTime || a.time;
const timeB = b.timeInfo?.primaryTime || b.time;
// Sort by time of day (earlier times first)
const timeComparison = timeA.getTime() - timeB.getTime();
if (timeComparison !== 0) {
return timeComparison;
}
// If times are equal, sort by task content for consistent ordering
return a.content.localeCompare(b.content);
});
@ -844,7 +845,7 @@ export class TimelineSidebarView extends ItemView {
events.forEach((event) => {
const time = event.timeInfo?.primaryTime || event.time;
const timeKey = this.getTimeGroupKey(time, event);
if (!timeGroups.has(timeKey)) {
timeGroups.set(timeKey, []);
}
@ -871,7 +872,7 @@ export class TimelineSidebarView extends ItemView {
// Use the formatted time component for precise grouping
return this.formatTimeComponent(event.timeInfo.timeComponent);
}
// Fallback to hour:minute format
return moment(time).format("HH:mm");
}
@ -881,20 +882,20 @@ export class TimelineSidebarView extends ItemView {
*/
private renderTimeGroup(containerEl: HTMLElement, timeKey: string, events: EnhancedTimelineEvent[]): void {
const groupEl = containerEl.createDiv("timeline-time-group");
// Time group header
const groupHeaderEl = groupEl.createDiv("timeline-time-group-header");
const timeEl = groupHeaderEl.createDiv("timeline-time-group-time");
timeEl.setText(timeKey);
timeEl.addClass("timeline-event-time");
timeEl.addClass("timeline-event-time-group");
const countEl = groupHeaderEl.createDiv("timeline-time-group-count");
countEl.setText(`${events.length} events`);
// Events in the group
const groupEventsEl = groupEl.createDiv("timeline-time-group-events");
events.forEach((event) => {
const eventEl = groupEventsEl.createDiv("timeline-event timeline-event-grouped");
eventEl.setAttribute("data-event-id", event.id);
@ -1132,7 +1133,7 @@ export class TimelineSidebarView extends ItemView {
// For canvas files, open directly
const leaf = this.app.workspace.getLeaf("tab");
await leaf.openFile(file);
this.app.workspace.setActiveLeaf(leaf, { focus: true });
this.app.workspace.setActiveLeaf(leaf, {focus: true});
return;
}
@ -1152,7 +1153,7 @@ export class TimelineSidebarView extends ItemView {
},
});
// Focus the editor after opening
this.app.workspace.setActiveLeaf(leafToUse, { focus: true });
this.app.workspace.setActiveLeaf(leafToUse, {focus: true});
}
private async handleQuickCapture(): Promise<void> {
@ -1191,7 +1192,7 @@ export class TimelineSidebarView extends ItemView {
);
if (todayEl) {
this.isAutoScrolling = true;
todayEl.scrollIntoView({ behavior: "smooth", block: "start" });
todayEl.scrollIntoView({behavior: "smooth", block: "start"});
setTimeout(() => {
this.isAutoScrolling = false;
}, 1000);
@ -1211,7 +1212,7 @@ export class TimelineSidebarView extends ItemView {
if (this.isAutoScrolling) return;
// Implement infinite scroll or lazy loading if needed
const { scrollTop, scrollHeight, clientHeight } =
const {scrollTop, scrollHeight, clientHeight} =
this.timelineContainerEl;
// Load more events when near bottom
@ -1230,7 +1231,7 @@ export class TimelineSidebarView extends ItemView {
task: Task,
event?: EnhancedTimelineEvent
): Promise<void> {
const updatedTask = { ...task, completed: !task.completed };
const updatedTask = {...task, completed: !task.completed};
if (updatedTask.completed) {
updatedTask.metadata.completedDate = Date.now();
@ -1259,7 +1260,7 @@ export class TimelineSidebarView extends ItemView {
taskId: task.id,
updates: updatedTask,
});
if (!result.success) {
console.error("Failed to toggle task completion:", result.error);
return;

View file

@ -26,7 +26,7 @@ import {
} from "@/components/features/task/view/details";
import "../styles/view.css";
import TaskProgressBarPlugin from "../index";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModal";
import { QuickCaptureModal } from "@/components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
import { t } from "@/translations/helper";
import {
getViewSettingOrDefault,

View file

@ -240,7 +240,7 @@
.quick-capture-modal.quick-capture-file {
width: 800px;
max-width: 90vw;
height: 750px;
height: 700px;
max-height: 80vh;
min-height: fit-content;
}
@ -515,6 +515,10 @@
color: var(--text-normal);
}
.quick-capture-modal .modal-title:has(.is-hidden) {
display: none;
}
.quick-capture-modal .modal-title {
display: flex;
align-items: center;

File diff suppressed because one or more lines are too long