Files
immerse/src/modals.ts

550 lines
17 KiB
TypeScript

import {
App,
Modal,
Notice,
Setting,
} from 'obsidian';
import { ImmerseTask } from './types';
import ImmersePlugin from './main';
// ============ Quick Add Task Modal ============
export class QuickAddTaskModal extends Modal {
plugin: ImmersePlugin;
taskText: string = '';
estimatedMinutes: number;
selectedList: string = 'work';
scheduledDate: string = '';
scheduledTime: string = '';
reminderMinutes: number = 0;
constructor(app: App, plugin: ImmersePlugin) {
super(app);
this.plugin = plugin;
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
if (plugin.settings.lists.length > 0) {
this.selectedList = plugin.settings.lists[0].id;
}
}
onOpen() {
const { contentEl } = this;
contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '⚡ Add New Task' });
// Task text input
new Setting(contentEl)
.setName('Task')
.addText(text => {
text.setPlaceholder('What do you need to do?')
.onChange(value => this.taskText = value);
text.inputEl.focus();
text.inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && this.taskText.trim()) {
this.submitTask();
}
});
});
// Time estimate
new Setting(contentEl)
.setName('Estimated Time')
.setDesc('How long do you think this will take?')
.addDropdown(dropdown => {
const options: Record<string, string> = {
'5': '5 min',
'10': '10 min',
'15': '15 min',
'20': '20 min',
'25': '25 min (1 pomodoro)',
'30': '30 min',
'45': '45 min',
'50': '50 min (2 pomodoros)',
'60': '1 hour',
'90': '1.5 hours',
'120': '2 hours',
'180': '3 hours',
};
Object.entries(options).forEach(([value, label]) => {
dropdown.addOption(value, label);
});
dropdown.setValue(this.estimatedMinutes.toString());
dropdown.onChange(value => this.estimatedMinutes = parseInt(value));
});
// List selection
new Setting(contentEl)
.setName('List')
.addDropdown(dropdown => {
this.plugin.settings.lists.forEach(list => {
dropdown.addOption(list.id, `${list.icon} ${list.name}`);
});
dropdown.setValue(this.selectedList);
dropdown.onChange(value => this.selectedList = value);
});
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.scheduledDate)
.onChange(value => this.scheduledDate = value);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.scheduledTime)
.onChange(value => this.scheduledTime = value);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue(this.reminderMinutes.toString());
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
});
}
// Buttons
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close());
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'immerse-btn immerse-btn-primary' });
addBtn.addEventListener('click', () => this.submitTask());
}
submitTask() {
if (this.taskText.trim()) {
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
// Add scheduling data if provided
if (this.scheduledDate) {
task.scheduledDate = this.scheduledDate;
}
if (this.scheduledTime) {
task.scheduledTime = this.scheduledTime;
}
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
task.reminderMinutes = this.reminderMinutes;
}
this.plugin.addTask(task);
new Notice('✅ Task added!');
this.close();
} else {
new Notice('Please enter a task description');
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// ============ Edit Task Modal ============
export class EditTaskModal extends Modal {
plugin: ImmersePlugin;
task: ImmerseTask;
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
super(app);
this.plugin = plugin;
this.task = { ...task };
}
onOpen() {
const { contentEl } = this;
contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '✏️ Edit Task' });
new Setting(contentEl)
.setName('Task')
.addText(text => text
.setValue(this.task.text)
.onChange(value => this.task.text = value));
new Setting(contentEl)
.setName('Estimated Time')
.addDropdown(dropdown => {
const options: Record<string, string> = {
'5': '5 min',
'10': '10 min',
'15': '15 min',
'20': '20 min',
'25': '25 min',
'30': '30 min',
'45': '45 min',
'50': '50 min',
'60': '1 hour',
'90': '1.5 hours',
'120': '2 hours',
'180': '3 hours',
};
Object.entries(options).forEach(([value, label]) => {
dropdown.addOption(value, label);
});
dropdown.setValue(this.task.estimatedMinutes.toString());
dropdown.onChange(value => this.task.estimatedMinutes = parseInt(value));
});
new Setting(contentEl)
.setName('List')
.addDropdown(dropdown => {
this.plugin.settings.lists.forEach(list => {
dropdown.addOption(list.id, `${list.icon} ${list.name}`);
});
dropdown.setValue(this.task.list);
dropdown.onChange(value => this.task.list = value);
});
new Setting(contentEl)
.setName('Notes')
.setDesc('Add any additional details or links')
.addTextArea(textarea => {
textarea
.setValue(this.task.notes)
.onChange(value => this.task.notes = value);
textarea.inputEl.rows = 4;
});
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.task.scheduledDate || '')
.onChange(value => this.task.scheduledDate = value || undefined);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.task.scheduledTime || '')
.onChange(value => this.task.scheduledTime = value || undefined);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue((this.task.reminderMinutes || 0).toString());
dropdown.onChange(value => {
const minutes = parseInt(value);
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
});
});
}
// Show actual time if task has been worked on
if (this.task.actualMinutes > 0) {
new Setting(contentEl)
.setName('Time Tracked')
.setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
}
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close());
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'immerse-btn immerse-btn-primary' });
saveBtn.addEventListener('click', () => {
this.plugin.updateTask(this.task.id, this.task);
new Notice('✅ Task updated!');
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// ============ Report Modal ============
export class ReportModal extends Modal {
plugin: ImmersePlugin;
startDate: string;
endDate: string;
selectedListIds: string[] = [];
constructor(app: App, plugin: ImmersePlugin) {
super(app);
this.plugin = plugin;
// Default to last 7 days
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
this.startDate = weekAgo.toISOString().split('T')[0];
}
onOpen() {
const { contentEl } = this;
contentEl.addClass('immerse-modal', 'immerse-report-modal');
contentEl.empty();
// Header
const header = contentEl.createEl('div', { cls: 'immerse-report-header' });
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
// Filters section
this.renderFilters(contentEl);
// Generate button
const generateBtn = contentEl.createEl('button', {
text: '🔄 Generate Report',
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
});
generateBtn.addEventListener('click', () => this.renderReport(contentEl));
// Initial report render
this.renderReport(contentEl);
}
renderFilters(container: Element) {
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
// Date range
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
new Setting(dateRow)
.setName('Start Date')
.addText(text => {
text.setValue(this.startDate)
.onChange(value => this.startDate = value);
text.inputEl.type = 'date';
});
new Setting(dateRow)
.setName('End Date')
.addText(text => {
text.setValue(this.endDate)
.onChange(value => this.endDate = value);
text.inputEl.type = 'date';
});
// Quick filters
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
const filters = [
{ label: 'Today', days: 0 },
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 30 days', days: 30 },
{ label: 'Last 90 days', days: 90 },
];
filters.forEach(filter => {
const btn = quickFilters.createEl('button', {
text: filter.label,
cls: 'immerse-quick-filter-btn'
});
btn.addEventListener('click', () => {
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const startDate = new Date(today);
startDate.setDate(startDate.getDate() - filter.days);
this.startDate = startDate.toISOString().split('T')[0];
this.renderReport(container);
});
});
}
renderReport(container: Element) {
// Remove old report if exists
const oldReport = container.querySelector('.immerse-report-content');
if (oldReport) oldReport.remove();
// Generate report data
const filters: import('./types').ReportFilters = {
startDate: this.startDate,
endDate: this.endDate,
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
};
const reportData = this.plugin.generateReport(filters);
// Create report content
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
// Check if we have data
if (reportData.totalTasks === 0) {
reportContent.createEl('div', {
text: 'No data available for the selected period. Complete some tasks to see your stats!',
cls: 'immerse-no-data-message'
});
return;
}
// Summary stats
this.renderSummaryStats(reportContent, reportData);
// Time by list (donut chart)
if (reportData.timeByList.length > 0) {
this.renderTimeByList(reportContent, reportData);
}
// Productivity insights
this.renderInsights(reportContent, reportData);
// Daily breakdown (bar chart - simplified text version)
if (reportData.dailyBreakdown.length > 0) {
this.renderDailyBreakdown(reportContent, reportData);
}
}
renderSummaryStats(container: Element, data: import('./types').ReportData) {
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
const stats = [
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
];
stats.forEach(stat => {
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
});
}
renderTimeByList(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
data.timeByList.forEach(item => {
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
// List info
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
// Progress bar
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
progress.style.width = `${item.percentage}%`;
progress.style.backgroundColor = item.listColor;
// Stats
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
stats.createEl('span', {
text: `${item.taskCount} tasks`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${item.percentage.toFixed(1)}%`,
cls: 'immerse-list-stat-percentage'
});
});
}
renderInsights(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
if (data.mostProductiveHour !== undefined) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
card.createEl('div', {
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
cls: 'immerse-insight-value'
});
}
if (data.mostProductiveDay) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
card.createEl('div', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
}
if (data.mostProductiveMonth) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
card.createEl('div', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
}
}
renderDailyBreakdown(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
const table = section.createEl('table', { cls: 'immerse-daily-table' });
const thead = table.createEl('thead');
const headerRow = thead.createEl('tr');
headerRow.createEl('th', { text: 'Date' });
headerRow.createEl('th', { text: 'Tasks' });
headerRow.createEl('th', { text: 'Hours' });
headerRow.createEl('th', { text: 'Pomodoros' });
const tbody = table.createEl('tbody');
// Show last 14 days max
const recentData = data.dailyBreakdown.slice(-14);
recentData.forEach(day => {
const row = tbody.createEl('tr');
row.createEl('td', { text: day.date });
row.createEl('td', { text: day.tasks.toString() });
row.createEl('td', { text: day.hours.toFixed(1) });
row.createEl('td', { text: day.pomodoros.toString() });
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}