mirror of
https://github.com/l7cy/obsidian-dynamic-timetable.git
synced 2026-07-22 07:40:29 +00:00
Compare commits
33 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb10eb8dc | ||
|
|
6172371b21 | ||
|
|
6c9a235128 | ||
|
|
e7e8551679 | ||
|
|
4e1610ae66 | ||
|
|
f4f2169655 | ||
|
|
f3ec100666 | ||
|
|
bff62e9fa5 | ||
|
|
7a47c945dd | ||
|
|
7fa1fc4e6c | ||
|
|
c94dc7b0cc | ||
|
|
6111557b94 | ||
|
|
59e71f9d61 | ||
|
|
9cb19298ce | ||
|
|
110f78637f | ||
|
|
5c30e5cf57 | ||
|
|
b647a43fdb | ||
|
|
d7d06699ed | ||
|
|
6b47c56ec5 | ||
|
|
b2ff931385 | ||
|
|
bba41f4520 | ||
|
|
7666536ddd | ||
|
|
33d1a5739e | ||
|
|
68450a8f8b | ||
|
|
95b13dbedc | ||
|
|
92f728207c | ||
|
|
e8091de245 | ||
|
|
584c7f3191 | ||
|
|
64c47dd54d | ||
|
|
37322ba811 | ||
|
|
f77b02ce91 | ||
|
|
acc0f69096 | ||
|
|
f17f8b7537 |
13 changed files with 307 additions and 68 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dynamic-timetable",
|
||||
"name": "Dynamic Timetable",
|
||||
"version": "4.4.1",
|
||||
"version": "4.6.1",
|
||||
"minAppVersion": "1.2.8",
|
||||
"description": "Calculate the estimated time of completion from the estimated time of the task and dynamically create a timetable.",
|
||||
"author": "L7Cy",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dynamic-timetable",
|
||||
"name": "Dynamic Timetable",
|
||||
"version": "4.4.1",
|
||||
"version": "4.6.1",
|
||||
"minAppVersion": "1.2.8",
|
||||
"description": "Calculate the estimated time of completion from the estimated time of the task and dynamically create a timetable.",
|
||||
"author": "L7Cy",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dynamic-timetable",
|
||||
"version": "4.4.1",
|
||||
"version": "4.6.1",
|
||||
"description": "Calculate the estimated time of completion from the estimated time of the task and dynamically create a timetable.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,17 @@ type BufferTimeRowProps = {
|
|||
bufferTime: number | null;
|
||||
};
|
||||
|
||||
const formatTime = (minutes: number): string => {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h${remainingMinutes}min`;
|
||||
};
|
||||
|
||||
const BufferTimeRow: React.FC<BufferTimeRowProps> = ({ bufferTime }) => (
|
||||
<tr className="buffer-time dt-buffer-time">
|
||||
<td>Buffer Time</td>
|
||||
<td colSpan={3} style={{ textAlign: 'center' }}>
|
||||
{bufferTime ? bufferTime : 0}min
|
||||
{bufferTime !== null ? formatTime(bufferTime) : '0h0min'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,21 +23,17 @@ export class DynamicTimetableSettingTab extends PluginSettingTab {
|
|||
'showStartTimeInTaskName'
|
||||
);
|
||||
this.createToggleSetting(
|
||||
'Show Category Names in Task',
|
||||
'showCategoryNamesInTask',
|
||||
'If enabled, displays category names within the task name.'
|
||||
'Show Categories in Task Name',
|
||||
'showCategoryNamesInTask'
|
||||
);
|
||||
this.createToggleSetting('Show Buffer Time Rows', 'showBufferTime');
|
||||
this.createToggleSetting('Show Completed Tasks', 'showCompletedTasks');
|
||||
this.createToggleSetting(
|
||||
'Show Completed Tasks',
|
||||
'showCompletedTasks',
|
||||
'If enabled, displays completed tasks in the timetable.'
|
||||
);
|
||||
this.createToggleSetting(
|
||||
'Show Progress Bar',
|
||||
'showProgressBar',
|
||||
'If enabled, displays a progress bar based on the top task estimate.'
|
||||
'Show Remaining Time',
|
||||
'showRemainingTime',
|
||||
'Show remaining time instead of the time for current task in progress.'
|
||||
);
|
||||
this.createToggleSetting('Show Progress Bar', 'showProgressBar');
|
||||
if (this.plugin.settings.showProgressBar) {
|
||||
this.createTextSetting(
|
||||
'Interval Time (Seconds)',
|
||||
|
|
@ -64,10 +60,43 @@ export class DynamicTimetableSettingTab extends PluginSettingTab {
|
|||
'Enter a regex that matches the delimiter for a new day.',
|
||||
'^---$'
|
||||
);
|
||||
this.createTextSetting(
|
||||
'Show Until Regex',
|
||||
'showUntilRegex',
|
||||
'Enter a regex. Tasks will be shown until a line matching this regex is found.'
|
||||
);
|
||||
const headerNames = Array.isArray(this.plugin.settings.headerNames)
|
||||
? this.plugin.settings.headerNames.join(', ')
|
||||
: '';
|
||||
this.createHeaderNamesSetting(headerNames);
|
||||
const pathToDictionaryDesc = this.containerEl.createEl('p');
|
||||
pathToDictionaryDesc.style.marginBlockStart = '0em';
|
||||
pathToDictionaryDesc.style.marginBlockEnd = '0em';
|
||||
|
||||
pathToDictionaryDesc.appendChild(
|
||||
createEl('span', {
|
||||
text: 'Enter the path to the custom dictionary file for ',
|
||||
})
|
||||
);
|
||||
pathToDictionaryDesc.appendChild(
|
||||
createEl('a', {
|
||||
text: 'Various Complements',
|
||||
href: 'obsidian://show-plugin?id=various-complements',
|
||||
})
|
||||
);
|
||||
pathToDictionaryDesc.appendText('.');
|
||||
this.createTextSetting(
|
||||
'Path to Dictionary for Suggestions',
|
||||
'pathToDictionary',
|
||||
pathToDictionaryDesc,
|
||||
'path/to/dictionary.md'
|
||||
);
|
||||
this.createTextAreaSetting(
|
||||
'Custom URL Scheme',
|
||||
'customUrlScheme',
|
||||
'Enter the URL scheme you want to execute when a task is completed. You can use the following placeholders: {{minutes}}, {{seconds}}, {{taskName}}',
|
||||
'your-app-scheme://doSomething?minutes={{minutes}}&seconds={{seconds}}&taskName={{taskName}}'
|
||||
);
|
||||
this.createToggleSetting(
|
||||
'Apply Background Color by Category (tag)',
|
||||
'applyBackgroundColorByCategory',
|
||||
|
|
@ -82,7 +111,7 @@ export class DynamicTimetableSettingTab extends PluginSettingTab {
|
|||
createTextSetting(
|
||||
name: string,
|
||||
key: string,
|
||||
desc?: string,
|
||||
desc?: any,
|
||||
placeholder?: string
|
||||
) {
|
||||
const setting = new Setting(this.containerEl).setName(name);
|
||||
|
|
@ -101,6 +130,29 @@ export class DynamicTimetableSettingTab extends PluginSettingTab {
|
|||
});
|
||||
}
|
||||
|
||||
createTextAreaSetting(
|
||||
name: string,
|
||||
key: string,
|
||||
desc?: string,
|
||||
placeholder?: string
|
||||
) {
|
||||
const setting = new Setting(this.containerEl).setName(name);
|
||||
if (desc) {
|
||||
setting.setDesc(desc);
|
||||
}
|
||||
setting.addTextArea((text) => {
|
||||
const el = text
|
||||
.setPlaceholder(placeholder || '')
|
||||
.setValue((this.plugin.settings[key] as string) || '');
|
||||
el.inputEl.style.height = '60px';
|
||||
el.inputEl.addEventListener('blur', async (event) => {
|
||||
const value = (event.target as HTMLTextAreaElement).value;
|
||||
await this.plugin.updateSetting(key, value);
|
||||
});
|
||||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
createToggleSetting(name: string, key: string, desc?: string) {
|
||||
const setting = new Setting(this.containerEl).setName(name);
|
||||
if (desc) {
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ export const StatisticsViewComponent = forwardRef((props: Props, ref) => {
|
|||
|
||||
const actualBackgroundColors = performanceArray.map((p) => {
|
||||
const color = categoryBackgroundColors[p.category];
|
||||
return color.replace(/,\s*([^,]+)\)/, ', 1)');
|
||||
return color ? color.replace(/,\s*([^,]+)\)/, ', 1)') : '';
|
||||
});
|
||||
const estimatedBackgroundColors = performanceArray.map((p) => {
|
||||
const color = categoryBackgroundColors[p.category];
|
||||
return color.replace(/,\s*([^,]+)\)/, ', 0.3)');
|
||||
return color ? color.replace(/,\s*([^,]+)\)/, ', 0.3)') : '';
|
||||
});
|
||||
|
||||
const actualTimes = performanceArray.map((p) => p.actualTime);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export const taskFunctions = (plugin: DynamicTimetable) => {
|
|||
const originalTaskName = taskMatch[1];
|
||||
const tags =
|
||||
line
|
||||
.match(/#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu)
|
||||
.match(/\s#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu)
|
||||
?.join(' ') || '';
|
||||
const actualStartTime = new Date(Date.now() - elapsedTime * 60 * 1000);
|
||||
|
||||
|
|
@ -135,6 +135,7 @@ export const taskFunctions = (plugin: DynamicTimetable) => {
|
|||
|
||||
let content = await plugin.app.vault.cachedRead(plugin.targetFile);
|
||||
const elapsedTime = getElapsedTime(content);
|
||||
await updateDictionaryFile(elapsedTime);
|
||||
const taskUpdate: TaskUpdate = { task, elapsedTime, remainingTime };
|
||||
|
||||
content = updateTaskInContent(content, taskUpdate);
|
||||
|
|
@ -150,6 +151,37 @@ export const taskFunctions = (plugin: DynamicTimetable) => {
|
|||
|
||||
const completeTask = async (task: Task) => {
|
||||
await updateTask(task, undefined);
|
||||
|
||||
if (!plugin.targetFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = await plugin.app.vault.cachedRead(plugin.targetFile);
|
||||
const taskParser = TaskParser.fromSettings(plugin.settings);
|
||||
let tasks: Task[] = taskParser.filterAndParseTasks(content);
|
||||
const nextUncompletedTask = tasks.find((t) => !t.isCompleted && t !== task);
|
||||
|
||||
if (nextUncompletedTask && plugin.settings.customUrlScheme) {
|
||||
let customUrl = plugin.settings.customUrlScheme;
|
||||
|
||||
const nextTaskName = nextUncompletedTask.task;
|
||||
const nextTaskEstimate = nextUncompletedTask.estimate;
|
||||
if (!nextTaskEstimate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTaskMinutes = parseInt(nextTaskEstimate);
|
||||
const nextTaskSeconds = nextTaskMinutes * 60;
|
||||
|
||||
customUrl = customUrl.replace('{{minutes}}', nextTaskMinutes.toString());
|
||||
customUrl = customUrl.replace('{{seconds}}', nextTaskSeconds.toString());
|
||||
customUrl = customUrl.replace(
|
||||
'{{taskName}}',
|
||||
encodeURIComponent(nextTaskName)
|
||||
);
|
||||
|
||||
window.open(customUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const interruptTask = async () => {
|
||||
|
|
@ -173,6 +205,102 @@ export const taskFunctions = (plugin: DynamicTimetable) => {
|
|||
await updateTask(firstUncompletedTask, remainingTime);
|
||||
};
|
||||
|
||||
const updateDictionaryFile = async (elapsedTime: number) => {
|
||||
if (!plugin.targetFile) {
|
||||
return;
|
||||
}
|
||||
const targetFileContent = await plugin.app.vault.cachedRead(
|
||||
plugin.targetFile
|
||||
);
|
||||
const taskParser = TaskParser.fromSettings(plugin.settings);
|
||||
let tasks: Task[] = taskParser.filterAndParseTasks(targetFileContent);
|
||||
const firstUncompletedTask = tasks.find((task) => !task.isCompleted);
|
||||
if (!firstUncompletedTask) return;
|
||||
|
||||
const dictionaryPath = plugin.settings.pathToDictionary;
|
||||
const dictionaryFile = plugin.app.metadataCache.getFirstLinkpathDest(
|
||||
dictionaryPath,
|
||||
'/'
|
||||
);
|
||||
|
||||
if (!dictionaryFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = await plugin.app.vault.cachedRead(dictionaryFile);
|
||||
|
||||
const escapeRegExp = (string: string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
const taskLineRegex = new RegExp(
|
||||
`^${escapeRegExp(firstUncompletedTask.originalTaskName)} ${
|
||||
plugin.settings.taskEstimateDelimiter
|
||||
}(.+)$`,
|
||||
'm'
|
||||
);
|
||||
const match = content.match(taskLineRegex);
|
||||
|
||||
let recentTimes: number[] = [];
|
||||
let trimmedMean = elapsedTime;
|
||||
let median = elapsedTime;
|
||||
|
||||
if (match) {
|
||||
const stats = match[1].split(',');
|
||||
if (stats.length > 2 && stats[2]) {
|
||||
const timesString = stats[2].split('%%')[1];
|
||||
if (timesString) {
|
||||
recentTimes = timesString.split('|').map(parseFloat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recentTimes.push(elapsedTime);
|
||||
|
||||
if (recentTimes.length > 20) {
|
||||
recentTimes.shift();
|
||||
}
|
||||
|
||||
trimmedMean = calculateTrimmedMean(recentTimes);
|
||||
median = calculateMedian(recentTimes);
|
||||
|
||||
const newLine = `${firstUncompletedTask.originalTaskName} ${
|
||||
plugin.settings.taskEstimateDelimiter
|
||||
}${trimmedMean},Trimmed Mean: ${trimmedMean} Median: ${median} Recent: ${elapsedTime},${
|
||||
firstUncompletedTask.task
|
||||
}%%${recentTimes.join('|')}%%`;
|
||||
|
||||
if (match) {
|
||||
content = content.replace(taskLineRegex, newLine);
|
||||
} else {
|
||||
content += '\n' + newLine;
|
||||
}
|
||||
|
||||
await plugin.app.vault.modify(dictionaryFile, content);
|
||||
await (plugin.app as any).commands.executeCommandById(
|
||||
'various-complements:reload-custom-dictionaries'
|
||||
);
|
||||
};
|
||||
|
||||
const calculateMedian = (values: number[]): number => {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
const median =
|
||||
sorted.length % 2 !== 0
|
||||
? sorted[mid]
|
||||
: (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
return Math.ceil(median);
|
||||
};
|
||||
|
||||
const calculateTrimmedMean = (values: number[]): number => {
|
||||
if (values.length <= 2) return Math.ceil(calculateMedian(values));
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
sorted.pop();
|
||||
sorted.shift();
|
||||
const trimmedMean = sorted.reduce((a, b) => a + b) / sorted.length;
|
||||
return Math.ceil(trimmedMean);
|
||||
};
|
||||
|
||||
return {
|
||||
initializeTasks,
|
||||
completeTask,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { DynamicTimetableSettings } from './main';
|
||||
|
||||
export interface Task {
|
||||
originalTaskName: string;
|
||||
task: string;
|
||||
startTime: Date | null;
|
||||
estimate: string | null;
|
||||
|
|
@ -12,6 +13,7 @@ export interface Task {
|
|||
|
||||
export class TaskParser {
|
||||
private dateDelimiter: RegExp;
|
||||
private showUntilRegex: RegExp;
|
||||
|
||||
constructor(
|
||||
private separator: string,
|
||||
|
|
@ -19,9 +21,13 @@ export class TaskParser {
|
|||
dateDelimiter: string,
|
||||
private showStartTimeInTaskName: boolean,
|
||||
private showEstimateInTaskName: boolean,
|
||||
private showCategoryNamesInTask: boolean
|
||||
private showCategoryNamesInTask: boolean,
|
||||
showUntilRegex: string
|
||||
) {
|
||||
this.dateDelimiter = dateDelimiter ? new RegExp(dateDelimiter) : /(?!x)x/;
|
||||
this.showUntilRegex = showUntilRegex
|
||||
? new RegExp(showUntilRegex)
|
||||
: /(?!x)x/;
|
||||
}
|
||||
|
||||
static fromSettings(settings: DynamicTimetableSettings): TaskParser {
|
||||
|
|
@ -31,7 +37,8 @@ export class TaskParser {
|
|||
settings.dateDelimiter,
|
||||
settings.showStartTimeInTaskName,
|
||||
settings.showEstimateInTaskName,
|
||||
settings.showCategoryNamesInTask
|
||||
settings.showCategoryNamesInTask,
|
||||
settings.showUntilRegex
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -40,18 +47,24 @@ export class TaskParser {
|
|||
let firstUncompletedTaskFound = false;
|
||||
let nextDay = 0;
|
||||
let dateDelimiterFound = false;
|
||||
let stopParsing = false;
|
||||
|
||||
const yamlStartTime = this.getYamlStartTime(content);
|
||||
const tasks = content
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.reduce((acc: Task[], task) => {
|
||||
if (stopParsing) return acc;
|
||||
if (this.isDateDelimiterLine(task)) {
|
||||
if (firstUncompletedTaskFound) {
|
||||
dateDelimiterFound = true;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
if (this.showUntilRegex.test(task)) {
|
||||
stopParsing = true;
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
!task.startsWith('- [ ]') &&
|
||||
|
|
@ -68,7 +81,7 @@ export class TaskParser {
|
|||
task.startsWith('- [x]') ||
|
||||
task.startsWith('+ [x]') ||
|
||||
task.startsWith('* [x]');
|
||||
const taskName = this.parseTaskName(task);
|
||||
const { taskName, originalTaskName } = this.parseTaskName(task);
|
||||
|
||||
if (dateDelimiterFound) {
|
||||
nextDay++;
|
||||
|
|
@ -97,6 +110,7 @@ export class TaskParser {
|
|||
|
||||
if (estimate) {
|
||||
acc.push({
|
||||
originalTaskName: originalTaskName,
|
||||
task: taskName,
|
||||
startTime: startTime,
|
||||
estimate: estimate,
|
||||
|
|
@ -117,10 +131,26 @@ export class TaskParser {
|
|||
return this.dateDelimiter.test(line);
|
||||
}
|
||||
|
||||
public parseTaskName(taskName: string): string {
|
||||
public parseTaskName(taskName: string): {
|
||||
taskName: string;
|
||||
originalTaskName: string;
|
||||
} {
|
||||
const taskNameRegex = /^[-+*]\s*\[\s*.\s*\]\s*/;
|
||||
const linkRegex = /\[\[([^\[\]]*\|)?([^\[\]]+)\]\]/g;
|
||||
const markdownLinkRegex = /\[([^\[\]]+)\]\(.+?\)/g;
|
||||
const estimateRegex = new RegExp(`\\${this.separator}\\s*\\d+\\s*`);
|
||||
const startTimeRegex = new RegExp(
|
||||
`\\${this.startTimeDelimiter}\\s*(?:\\d{4}-\\d{2}-\\d{2}T)?(\\d{1,2}:?\\d{2})`
|
||||
);
|
||||
const categoryRegex = /\s#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu;
|
||||
|
||||
let originalTaskName = taskName;
|
||||
originalTaskName = originalTaskName
|
||||
.replace(taskNameRegex, '')
|
||||
.replace(estimateRegex, '')
|
||||
.replace(startTimeRegex, '')
|
||||
.replace(categoryRegex, '')
|
||||
.trim();
|
||||
|
||||
taskName = taskName
|
||||
.replace(taskNameRegex, '')
|
||||
|
|
@ -129,14 +159,8 @@ export class TaskParser {
|
|||
.replace(markdownLinkRegex, '$1')
|
||||
.trim();
|
||||
|
||||
const startTimeRegex = new RegExp(
|
||||
`\\${this.startTimeDelimiter}\\s*(?:\\d{4}-\\d{2}-\\d{2}T)?(\\d{1,2}:?\\d{2})`
|
||||
);
|
||||
|
||||
if (!this.showCategoryNamesInTask) {
|
||||
taskName = taskName
|
||||
.replace(/#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu, '')
|
||||
.trim();
|
||||
taskName = taskName.replace(categoryRegex, '').trim();
|
||||
}
|
||||
|
||||
if (this.showStartTimeInTaskName) {
|
||||
|
|
@ -149,11 +173,10 @@ export class TaskParser {
|
|||
}
|
||||
|
||||
if (!this.showEstimateInTaskName) {
|
||||
const estimateRegex = new RegExp(`\\${this.separator}\\s*\\d+\\s*`);
|
||||
taskName = taskName.replace(estimateRegex, '').trim();
|
||||
}
|
||||
|
||||
return taskName;
|
||||
return { taskName, originalTaskName };
|
||||
}
|
||||
|
||||
public parseStartTime(task: string, nextDay: number): Date | null {
|
||||
|
|
@ -201,7 +224,7 @@ export class TaskParser {
|
|||
}
|
||||
|
||||
private parseCategories(taskName: string): string[] {
|
||||
const tagRegex = /#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu;
|
||||
const tagRegex = /\s#([^\s!#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]+)/gu;
|
||||
const categories = [];
|
||||
let match;
|
||||
while ((match = tagRegex.exec(taskName)) !== null) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ type TaskRowProps = {
|
|||
firstUncompletedTaskRef: React.MutableRefObject<HTMLTableRowElement | null> | null;
|
||||
categoryBackgroundColors: Record<string, string>;
|
||||
allTasksCompleted: boolean;
|
||||
duration: number;
|
||||
estimate: number;
|
||||
};
|
||||
|
||||
const formatDateToTime = (date: Date) => {
|
||||
|
|
@ -17,6 +19,17 @@ const formatDateToTime = (date: Date) => {
|
|||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const formatToHHMMSS = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const remainingSeconds = (seconds % 60).toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}:${remainingSeconds}`;
|
||||
};
|
||||
|
||||
const createCategoryClasses = (categories: string[]): string => {
|
||||
return categories.map((category) => `dt-category-${category}`).join(' ');
|
||||
};
|
||||
|
|
@ -28,19 +41,26 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||
firstUncompletedTaskRef,
|
||||
categoryBackgroundColors,
|
||||
allTasksCompleted,
|
||||
duration,
|
||||
estimate,
|
||||
}) => {
|
||||
let bufferClass = '';
|
||||
if (
|
||||
task.originalStartTime &&
|
||||
bufferTime !== null &&
|
||||
0 <= bufferTime &&
|
||||
!task.isCompleted
|
||||
(task.originalStartTime &&
|
||||
bufferTime !== null &&
|
||||
0 <= bufferTime &&
|
||||
!task.isCompleted) ||
|
||||
(0 <= estimate - duration && firstUncompletedTaskRef)
|
||||
) {
|
||||
bufferClass = 'on-time';
|
||||
} else if (bufferTime !== null && bufferTime < 0 && !task.isCompleted) {
|
||||
} else if (
|
||||
(bufferTime !== null && bufferTime < 0 && !task.isCompleted) ||
|
||||
(estimate - duration < 0 && firstUncompletedTaskRef)
|
||||
) {
|
||||
bufferClass = 'late';
|
||||
}
|
||||
|
||||
const remainingTimeSeconds = Math.floor((estimate - duration) / 1000);
|
||||
const categoryClasses = createCategoryClasses(task.categories);
|
||||
const originalBackgroundColor =
|
||||
categoryBackgroundColors[task.categories[0]] || '';
|
||||
|
|
@ -58,22 +78,31 @@ const TaskRow: React.FC<TaskRowProps> = ({
|
|||
return (
|
||||
<tr
|
||||
ref={task.isCompleted ? null : firstUncompletedTaskRef}
|
||||
className={`${bufferClass} ${
|
||||
className={`dt-task-row ${bufferClass} ${
|
||||
!allTasksCompleted && task.isCompleted ? 'dt-completed' : ''
|
||||
} ${categoryClasses}`}
|
||||
style={style}>
|
||||
<td>{task.task}</td>
|
||||
{plugin.settings.showEstimate && (
|
||||
<td style={{ textAlign: 'center' }}>{task.estimate}</td>
|
||||
)}
|
||||
{plugin.settings.showStartTime && (
|
||||
{plugin.settings.showEstimate &&
|
||||
!(plugin.settings.showRemainingTime && firstUncompletedTaskRef) && (
|
||||
<td style={{ textAlign: 'center' }}>{task.estimate}</td>
|
||||
)}
|
||||
{plugin.settings.showStartTime &&
|
||||
!(plugin.settings.showRemainingTime && firstUncompletedTaskRef) && (
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
{task.startTime ? formatDateToTime(task.startTime) : ''}
|
||||
</td>
|
||||
)}
|
||||
{!(plugin.settings.showRemainingTime && firstUncompletedTaskRef) && (
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
{task.startTime ? formatDateToTime(task.startTime) : ''}
|
||||
{task.endTime ? formatDateToTime(task.endTime) : ''}
|
||||
</td>
|
||||
)}
|
||||
{plugin.settings.showRemainingTime && firstUncompletedTaskRef && (
|
||||
<td colSpan={3} style={{ textAlign: 'center' }}>
|
||||
{formatToHHMMSS(Math.abs(remainingTimeSeconds))}
|
||||
</td>
|
||||
)}
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
{task.endTime ? formatDateToTime(task.endTime) : ''}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@ import React from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { WorkspaceLeaf, ItemView } from 'obsidian';
|
||||
import DynamicTimetable from './main';
|
||||
import TimetableViewComponent, {
|
||||
TimetableViewComponentRef,
|
||||
} from './TimetableViewComponent';
|
||||
import TimetableViewComponent from './TimetableViewComponent';
|
||||
import { CommandsManager } from './Commands';
|
||||
|
||||
export class TimetableView extends ItemView {
|
||||
private readonly plugin: DynamicTimetable;
|
||||
private componentRef: React.RefObject<TimetableViewComponentRef>;
|
||||
private root: any;
|
||||
private commandsManager: CommandsManager;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: DynamicTimetable) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.componentRef = React.createRef<TimetableViewComponentRef>();
|
||||
this.commandsManager = new CommandsManager(plugin);
|
||||
this.icon = "table";
|
||||
this.icon = 'table';
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
|
|
@ -33,7 +29,7 @@ export class TimetableView extends ItemView {
|
|||
this.root = createRoot(this.containerEl);
|
||||
this.root.render(
|
||||
<TimetableViewComponent
|
||||
ref={this.componentRef}
|
||||
ref={this.plugin.timetableViewComponentRef}
|
||||
plugin={this.plugin}
|
||||
commandsManager={this.commandsManager}
|
||||
/>
|
||||
|
|
@ -47,8 +43,8 @@ export class TimetableView extends ItemView {
|
|||
}
|
||||
|
||||
async update() {
|
||||
if (this.componentRef.current) {
|
||||
this.componentRef.current.update();
|
||||
if (this.plugin.timetableViewComponentRef.current) {
|
||||
this.plugin.timetableViewComponentRef.current.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import ProgressBar from './ProgressBar';
|
|||
import { CommandsManager } from './Commands';
|
||||
import BufferTimeRow from './BufferTimeRow';
|
||||
import TaskRow from './TaskRow';
|
||||
import { MarkdownView, Notice } from 'obsidian';
|
||||
import { Notice } from 'obsidian';
|
||||
import {
|
||||
convertHexToHSLA,
|
||||
getHSLAColorForCategory,
|
||||
|
|
@ -70,10 +70,6 @@ const TimetableViewComponent = forwardRef<
|
|||
|
||||
const performScroll = () => {
|
||||
if (firstUncompletedTaskRef.current && containerRef.current) {
|
||||
const activeView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (activeView?.file === plugin.targetFile) {
|
||||
return;
|
||||
}
|
||||
const containerHeight = containerRef.current.offsetHeight;
|
||||
const taskOffsetTop = firstUncompletedTaskRef.current.offsetTop;
|
||||
const scrollToPosition = taskOffsetTop - containerHeight / 5;
|
||||
|
|
@ -119,10 +115,10 @@ const TimetableViewComponent = forwardRef<
|
|||
}
|
||||
newBackgroundColors[category] = color;
|
||||
document.documentElement.style.setProperty(`--${className}-bg`, color);
|
||||
if (!plugin.isCategoryColorsReady) {
|
||||
plugin.isCategoryColorsReady = true;
|
||||
}
|
||||
});
|
||||
if (!plugin.isCategoryColorsReady) {
|
||||
plugin.isCategoryColorsReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
setCategoryBackgroundColors(newBackgroundColors);
|
||||
|
|
@ -199,7 +195,7 @@ const TimetableViewComponent = forwardRef<
|
|||
|
||||
useEffect(() => {
|
||||
performScroll();
|
||||
}, [tasks]);
|
||||
}, [plugin.targetFile]);
|
||||
|
||||
useEffect(() => {
|
||||
updateBackgroundColors();
|
||||
|
|
@ -276,6 +272,8 @@ const TimetableViewComponent = forwardRef<
|
|||
task === firstUncompletedTask ? firstUncompletedTaskRef : null
|
||||
}
|
||||
allTasksCompleted={allTasksCompleted}
|
||||
duration={progressDuration}
|
||||
estimate={progressEstimate}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export interface DynamicTimetableSettings {
|
|||
showCategoryNamesInTask: boolean;
|
||||
categoryColors: { category: string; color: string }[];
|
||||
categoryTransparency: number;
|
||||
pathToDictionary: string;
|
||||
showRemainingTime: boolean;
|
||||
customUrlScheme: string;
|
||||
showUntilRegex: string;
|
||||
[key: string]:
|
||||
| string
|
||||
| boolean
|
||||
|
|
@ -68,6 +72,10 @@ export default class DynamicTimetable extends Plugin {
|
|||
showCategoryNamesInTask: false,
|
||||
categoryColors: [],
|
||||
categoryTransparency: 0.3,
|
||||
pathToDictionary: '',
|
||||
showRemainingTime: true,
|
||||
customUrlScheme: '',
|
||||
showUntilRegex: '',
|
||||
};
|
||||
|
||||
async onload() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
.on-time td,
|
||||
.late td {
|
||||
vertical-align: text-top;
|
||||
.dt-task-row {
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.late {
|
||||
|
|
|
|||
Loading…
Reference in a new issue