mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Refactor plugin architecture and implement best practices
- Create modular folder structure - Move classes to dedicated files - Extract utility functions - Improve error handling - Apply Obsidian plugin guidelines - Use setHeading for section headings - Use Vault API instead of adapter where possible - Simplify codebase with fewer console logs - Update frontmatter processing to use FileManager.processFrontMatter
This commit is contained in:
commit
b1c755ddfa
25 changed files with 5523 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
obsidian-developer-docs/
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
5
LICENSE
Normal file
5
LICENSE
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Copyright (C) 2020-2025 by Dynalist Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
111
README.md
Normal file
111
README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# ChronoSync for Obsidian
|
||||
|
||||
ChronoSync is a comprehensive diary, task, note, and time management plugin for Obsidian. It integrates calendar views, task management, daily notes, and timeblocking into a seamless workflow.
|
||||
|
||||
## Features
|
||||
|
||||
- **Enhanced Daily Notes**
|
||||
- Automatic creation and navigation to daily notes
|
||||
- YAML frontmatter for daily metadata (pomodoros, workout, meditate, tags, important)
|
||||
- Integrated timeblock section within daily notes
|
||||
|
||||
- **Advanced Task Management**
|
||||
- Tasks as individual Markdown notes with rich YAML frontmatter
|
||||
- Task properties: title, due date, priority, status, contexts, tags, recurrence, details
|
||||
- Dedicated task views and filtering
|
||||
|
||||
- **Calendar Views**
|
||||
- Month, Week, and Year views integrated into the Obsidian workspace
|
||||
- Visual cues on calendar dates for entries, due tasks, and metadata
|
||||
- Easy navigation to daily notes
|
||||
|
||||
- **Timeblocking**
|
||||
- Structured timeblock tables in daily notes
|
||||
- Easy creation and editing of timeblocks
|
||||
- Configurable time intervals and ranges
|
||||
|
||||
- **Flexible Layouts**
|
||||
- Side-by-side view (Calendar/List on one side, File Preview/Editor on the other)
|
||||
- Tabbed interface for switching between tasks, notes, and timeblocks
|
||||
|
||||
## Installation
|
||||
|
||||
### From Obsidian Community Plugins
|
||||
|
||||
1. Open Obsidian Settings
|
||||
2. Go to Community Plugins and turn off Safe Mode
|
||||
3. Click Browse and search for "ChronoSync"
|
||||
4. Install the plugin and enable it
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Download the latest release from the GitHub releases page
|
||||
2. Extract the files to your Obsidian vault's plugins folder: `<vault>/.obsidian/plugins/chronosync/`
|
||||
3. Reload Obsidian
|
||||
4. Go to Settings > Community Plugins and enable "ChronoSync"
|
||||
|
||||
## Usage
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. After installation, click the calendar icon in the ribbon to open the ChronoSync dashboard
|
||||
2. Configure the plugin settings in Settings > ChronoSync
|
||||
3. Create your first task or navigate to today's daily note
|
||||
|
||||
### Commands
|
||||
|
||||
ChronoSync adds several commands to Obsidian's command palette:
|
||||
|
||||
- **Open Dashboard/Calendar View**: Opens the main ChronoSync dashboard
|
||||
- **Create New Task**: Opens a modal to create a new task
|
||||
- **Go to Today's Note**: Navigates to or creates today's daily note
|
||||
- **Open Home Note**: Navigates to or creates your home note
|
||||
- **Increment Daily Pomodoros**: Adds 1 to the pomodoro count in today's note
|
||||
- **Toggle Daily Workout**: Toggles the workout flag in today's note
|
||||
- **Toggle Daily Meditation**: Toggles the meditation flag in today's note
|
||||
- **Toggle Daily Important Flag**: Toggles the important flag in today's note
|
||||
|
||||
### Folder Structure
|
||||
|
||||
ChronoSync uses the following folder structure by default (configurable in settings):
|
||||
|
||||
- `ChronoSync/Daily/`: Daily notes (YYYY-MM-DD.md)
|
||||
- `ChronoSync/Tasks/`: Task files
|
||||
- `ChronoSync/Notes/`: General notes
|
||||
- `ChronoSync/Home.md`: Home note
|
||||
|
||||
## Configuration
|
||||
|
||||
In the plugin settings, you can configure:
|
||||
|
||||
- Folder paths for daily notes, tasks, and general notes
|
||||
- Default task properties (priority, status)
|
||||
- Timeblock settings (start/end times, interval)
|
||||
- Indexing options
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [NodeJS](https://nodejs.org/) v16 or later
|
||||
- [npm](https://www.npmjs.com/)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone this repository
|
||||
2. Run `npm install` to install dependencies
|
||||
3. Run `npm run dev` to start the development build process
|
||||
|
||||
### Building
|
||||
|
||||
- `npm run dev`: Builds the plugin and watches for changes
|
||||
- `npm run build`: Builds the plugin for production
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by `diary-tui` by Callum Alpass
|
||||
- Built with the [Obsidian API](https://github.com/obsidianmd/obsidian-api)
|
||||
219
SPEC.md
Normal file
219
SPEC.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
## Obsidian Plugin Specification: "Chronosync" (or "Zenith Planner")
|
||||
|
||||
**Plugin Name Ideas:** Chronosync, Zenith Planner, Obsidian Task & Time Weaver, Daily Flow
|
||||
|
||||
**Author:** Callum Alpass
|
||||
|
||||
**Version:** 0.1.0 (Initial Release)
|
||||
|
||||
**Minimum Obsidian Version:** (To be determined based on API usage, likely 1.0.0+)
|
||||
|
||||
**Inspired By:** `diary-tui` by Callum Alpass
|
||||
|
||||
**1. Overview & Goal:**
|
||||
|
||||
Chronosync aims to integrate comprehensive diary, task, note, and time management directly within Obsidian, leveraging Obsidian's strengths in linking and Markdown. It will provide calendar views, robust task management with YAML frontmatter, daily timeblocking, and organized note-taking, mirroring the key functionalities of the `diary-tui` command-line application. The plugin will focus on enhancing daily planning, task tracking, and reflective journaling.
|
||||
|
||||
**2. Core Features:**
|
||||
|
||||
* **Enhanced Daily Notes:**
|
||||
* Automatic creation/navigation to daily notes.
|
||||
* YAML frontmatter for daily metadata (e.g., `pomodoros`, `workout`, `meditate`, `tags`, `important`).
|
||||
* Integrated Timeblock section within daily notes.
|
||||
* **Advanced Task Management:**
|
||||
* Tasks as individual Markdown notes with rich YAML frontmatter.
|
||||
* Task properties: title, due date, priority, status, contexts, tags, recurrence, details.
|
||||
* Dedicated Task views and filtering.
|
||||
* **Note Organization:**
|
||||
* View non-task notes associated with specific dates (based on `dateCreated` or a custom field).
|
||||
* Easy internal linking and navigation.
|
||||
* **Calendar Views:**
|
||||
* Month, Week, and Year views integrated into the Obsidian workspace.
|
||||
* Visual cues on calendar dates for entries, due tasks, and metadata.
|
||||
* **Flexible Layouts:**
|
||||
* Side-by-side view (Calendar/List on one side, File Preview/Editor on the other).
|
||||
* Full-pane views for focused work.
|
||||
* **Data Integrity & Performance:**
|
||||
* Efficient indexing and caching of task/note metadata.
|
||||
* Robust YAML parsing and updating.
|
||||
|
||||
**3. User Interface (UI) & Interaction:**
|
||||
|
||||
* **Ribbon Icon:**
|
||||
* An icon to open a primary Chronosync view (e.g., a dashboard showing calendar and upcoming tasks).
|
||||
* **Command Palette Commands:**
|
||||
* `Chronosync: Open Dashboard/Calendar View`
|
||||
* `Chronosync: Create New Task`
|
||||
* `Chronosync: Go to Today's Note`
|
||||
* `Chronosync: Open Home Note`
|
||||
* `Chronosync: Search Diary/Notes`
|
||||
* `Chronosync: Filter Tasks by Context`
|
||||
* `Chronosync: Toggle Daily Metadata (Meditate, Workout, Important)`
|
||||
* `Chronosync: Increment Daily Pomodoros`
|
||||
* `Chronosync: Show Weekly/Monthly Stats`
|
||||
* `Chronosync: Re-index Tasks & Notes`
|
||||
* **Custom Views/Panes (as new Obsidian View Types):**
|
||||
* **Calendar View:**
|
||||
* Switchable between Month, Week, Year.
|
||||
* Clicking a date navigates to the daily note or shows a summary.
|
||||
* Visual indicators on dates (dots/icons) for:
|
||||
* Existing daily note.
|
||||
* Tasks due.
|
||||
* Specific metadata (e.g., 'important' tag).
|
||||
* **Task List View:**
|
||||
* Displays all tasks, filterable by status (open, in-progress, done, all, archive), context, due date, priority.
|
||||
* Sortable by due date, priority, title.
|
||||
* Clicking a task opens it in the editor or a preview pane.
|
||||
* Context menu/buttons for actions: toggle status, change priority, edit, delete, archive.
|
||||
* **Notes View:**
|
||||
* Displays non-task notes, filterable by date created (matching selected calendar date) or tags.
|
||||
* Clicking a note opens it.
|
||||
* **File Preview Pane:**
|
||||
* When a task or note is selected in a list view, its content is previewed (similar to Obsidian's linked pane).
|
||||
* **Modals:**
|
||||
* **Task Creation Modal:**
|
||||
* Fields: Title (required), Details (Markdown), Due Date (date picker), Priority (dropdown: low, normal, high), Context Tags (comma-separated text input), Extra Tags (comma-separated text input), Recurrence Frequency (dropdown: none, daily, weekly, monthly, yearly), Day of Month (text input, visible if monthly/yearly), Days of Week (checkboxes, visible if weekly).
|
||||
* Input validation for dates and required fields.
|
||||
* Confirmation step before creating the task file.
|
||||
* **Quick Add Note Modal (for daily notes):**
|
||||
* Simple text input to append a timestamped note to the current daily note.
|
||||
* **Timeblock Entry Modal:**
|
||||
* Input for activity for a selected time slot in the daily note's timeblock.
|
||||
* **Search/Filter Modals:**
|
||||
* Input fields for search queries, tag filters, context filters.
|
||||
* **Confirmation Dialogs:** For deletions, etc.
|
||||
* **Settings Tab:**
|
||||
* Configure folder paths (Daily Notes, Tasks, General Notes, Home Note).
|
||||
* Default task properties.
|
||||
* Editor preferences (though Obsidian's internal editor will be primary).
|
||||
* Timeblock template settings.
|
||||
* Indexing options (e.g., auto-reindex frequency).
|
||||
* Appearance settings for calendar indicators.
|
||||
|
||||
**4. Data Management & Storage:**
|
||||
|
||||
* **Folders:**
|
||||
* `Daily Notes Folder`: (User-configurable, e.g., `Chronosync/Daily/`) - Files named `YYYY-MM-DD.md`.
|
||||
* `Tasks Folder`: (User-configurable, e.g., `Chronosync/Tasks/`) - Task files.
|
||||
* `Notes Folder`: (User-configurable, e.g., `Chronosync/Notes/` or root) - For general notes.
|
||||
* `Home Note Path`: (User-configurable, e.g., `Chronosync/Home.md`).
|
||||
* **File Naming for Tasks:**
|
||||
* `YYYYMMDD<random_suffix>.md` (e.g., `250323abc.md`) to ensure uniqueness and sortability by creation.
|
||||
* **YAML Frontmatter (Daily Notes):**
|
||||
* `date: YYYY-MM-DD`
|
||||
* `pomodoros: number`
|
||||
* `workout: boolean`
|
||||
* `meditate: boolean`
|
||||
* `tags: [tag1, tag2]`
|
||||
* `important: boolean` (or could be a tag)
|
||||
* (Other user-defined metadata)
|
||||
* **YAML Frontmatter (Task Notes):**
|
||||
* `title: "Task Title"`
|
||||
* `zettelid: "YYYYMMDD<random_suffix>"` (matches filename base)
|
||||
* `dateCreated: "YYYY-MM-DDTHH:MM:SS"`
|
||||
* `dateModified: "YYYY-MM-DDTHH:MM:SS"`
|
||||
* `status: "open" | "in-progress" | "done"`
|
||||
* `due: "YYYY-MM-DD"` (optional)
|
||||
* `tags: ["task", "extra_tag1", "archive"]` (always includes "task")
|
||||
* `priority: "low" | "normal" | "high"`
|
||||
* `contexts: ["context1", "context2"]` (optional)
|
||||
* `recurrence:` (optional object)
|
||||
* `frequency: "daily" | "weekly" | "monthly" | "yearly"`
|
||||
* `days_of_week: ["mon", "tue"]` (if weekly)
|
||||
* `day_of_month: number` (if monthly/yearly)
|
||||
* `complete_instances: ["YYYY-MM-DD", "YYYY-MM-DD"]` (for recurring tasks)
|
||||
* **YAML Frontmatter (General Notes):**
|
||||
* `title: "Note Title"`
|
||||
* `dateCreated: "YYYY-MM-DDTHH:MM:SS"`
|
||||
* `dateModified: "YYYY-MM-DDTHH:MM:SS"`
|
||||
* `tags: [tag1, tag2]`
|
||||
* **Timeblocks:**
|
||||
* Stored as a Markdown table within the body of daily notes (e.g., under a `## Timeblock` heading).
|
||||
* Structure: `| Time | Activity |`
|
||||
* Default template will be configurable.
|
||||
* **Indexing:**
|
||||
* An `index_state.json` file (similar to `diary-tui`) in the plugin's data directory (`.obsidian/plugins/chronosync/data/`) to store file modification times/hashes for efficient re-indexing.
|
||||
* Background process for indexing tasks and notes.
|
||||
|
||||
**5. Key Functionalities (Detailed):**
|
||||
|
||||
* **Daily Note Enhancement:**
|
||||
* On plugin load or daily, check/create today's note.
|
||||
* Commands to toggle boolean metadata (`workout`, `meditate`, `important`) and increment `pomodoros` in the current daily note's frontmatter.
|
||||
* Command to add a default timeblock table to the daily note if not present.
|
||||
* Ability to edit timeblock entries directly in the Markdown table or via a modal.
|
||||
* **Task Creation & Management:**
|
||||
* **Creation:** Via Modal. File saved to Tasks Folder.
|
||||
* **Status Toggle:** `open` -> `in-progress` -> `done` -> `open`. For recurring tasks, marks the current instance as complete in `complete_instances` if due today.
|
||||
* **Priority Cycle:** `low` -> `normal` -> `high` -> `low`.
|
||||
* **Recurrence:**
|
||||
* When a recurring task instance is marked "done" for the `current_date`, add `YYYY-MM-DD` to `complete_instances`.
|
||||
* Task list will show recurring tasks as "open" if they are due for the `current_date` and not in `complete_instances`.
|
||||
* **Editing:** Opens the task note in Obsidian editor.
|
||||
* **Deletion:** Deletes the task file (with confirmation).
|
||||
* **Archiving:** Adds/removes an "archive" tag in the task's frontmatter. Archived tasks are hidden by default but viewable with a filter.
|
||||
* **Note Listing (Non-Task):**
|
||||
* In Notes View, list all notes from the `Notes Folder` (or entire vault, configurable) where `dateCreated` matches the selected calendar date. Exclude notes tagged "task".
|
||||
* **Calendar Views:**
|
||||
* Render calendar. Highlight today's date. Highlight selected date.
|
||||
* Mark dates with existing daily notes.
|
||||
* Mark dates with tasks due (color-coded by highest priority if multiple tasks).
|
||||
* Mark dates if the daily note has `important: true` or a specific "important" tag.
|
||||
* **Search & Filtering:**
|
||||
* **Global Search:** Input query, search content of all diary and note files. Results update calendar highlighting and can be navigated.
|
||||
* **Tag Filter (Diary):** Input tag, filter daily notes. Updates calendar.
|
||||
* **Context Filter (Tasks):** Input context, filter task list.
|
||||
* **Stats:**
|
||||
* Commands to show a modal with weekly/monthly stats (pomodoros, workouts, meditation days) aggregated from daily note frontmatter.
|
||||
|
||||
**6. Configuration Options (Settings Tab):**
|
||||
|
||||
* **General:**
|
||||
* `Daily Notes Folder Path` (text input, default: `Chronosync/Daily`)
|
||||
* `Tasks Folder Path` (text input, default: `Chronosync/Tasks`)
|
||||
* `General Notes Folder Path` (text input, default: `/` for vault root, or e.g., `Chronosync/Notes`)
|
||||
* `Home Note Path` (text input, default: `Chronosync/Home.md`)
|
||||
* **Tasks:**
|
||||
* `Default Task Priority` (dropdown: low, normal, high)
|
||||
* `Default Task Status` (dropdown: open)
|
||||
* **Timeblocks:**
|
||||
* `Default Timeblock Start Time` (e.g., "05:00")
|
||||
* `Default Timeblock End Time` (e.g., "23:30")
|
||||
* `Timeblock Interval` (dropdown: 30 minutes, 1 hour)
|
||||
* `Automatically add Timeblock to new Daily Notes` (toggle)
|
||||
* **Appearance:**
|
||||
* `Calendar: Daily Note Indicator` (e.g., dot color, icon)
|
||||
* `Calendar: Task Due Indicator` (e.g., dot color, icon - potentially different for priorities)
|
||||
* `Calendar: Important Day Indicator` (e.g., dot color, icon)
|
||||
* **Indexing:**
|
||||
* `Enable Background Indexing` (toggle, default: true)
|
||||
* `Re-index on Startup` (toggle, default: true)
|
||||
|
||||
**7. Dependencies & Potential Integrations:**
|
||||
|
||||
* **Dataview Plugin (Optional but Recommended):** Could be leveraged for advanced querying and dynamic list generation, potentially simplifying some custom view logic if the user has it installed. The plugin should aim to function without it for core features.
|
||||
* **Obsidian Tasks Plugin (Potential Integration):** For task rendering and potentially some actions, explore compatibility or an option to use its format/queries if available. This would be a more advanced integration.
|
||||
* **Calendar Plugin (Potential Integration):** Check if existing calendar plugins offer APIs that could be used or if this plugin's calendar should remain standalone.
|
||||
* **Moment.js (Bundled with Obsidian):** For date/time manipulations.
|
||||
|
||||
**8. Future Enhancements (Post v0.1.0):**
|
||||
|
||||
* More sophisticated recurrence options (e.g., "every 2nd Tuesday").
|
||||
* Kanban board view for tasks.
|
||||
* Gantt chart view for tasks with durations.
|
||||
* Directly link tasks to timeblock slots.
|
||||
* Notifications/reminders for due tasks (if Obsidian API allows).
|
||||
* Habit tracking features integrated with daily notes.
|
||||
* Customizable dashboard view.
|
||||
* Mobile compatibility improvements.
|
||||
* Templating support for new tasks/notes beyond frontmatter.
|
||||
|
||||
**9. Technical Considerations:**
|
||||
|
||||
* **Performance:** Efficient file reading and YAML parsing is critical. Caching metadata and diff-based indexing (checking mod times/hashes) will be essential.
|
||||
* **API Usage:** Rely on stable Obsidian API features.
|
||||
* **Error Handling:** Graceful handling of missing files, malformed YAML, and other potential issues.
|
||||
* **State Management:** Carefully manage the state of selected dates, filters, scroll positions, etc.
|
||||
|
||||
This spec should give you a solid blueprint, Callum. Given you wrote the original TUI, you'll have a great head start on the logic! Good luck with the development if you decide to pursue it!
|
||||
|
||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
2
main.ts
Normal file
2
main.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import ChronoSyncPlugin from './src/main';
|
||||
export default ChronoSyncPlugin;
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "chronosync",
|
||||
"name": "ChronoSync",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Comprehensive diary, task, note, and time management for Obsidian.",
|
||||
"author": "Callum Alpass",
|
||||
"authorUrl": "https://github.com/callumA",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
2457
package-lock.json
generated
Normal file
2457
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
package.json
Normal file
30
package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "chronosync",
|
||||
"version": "0.1.0",
|
||||
"description": "Comprehensive diary, task, note, and time management for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/date-fns": "^2.5.3",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/yaml": "^1.9.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^2.30.0",
|
||||
"yaml": "^2.3.1"
|
||||
}
|
||||
}
|
||||
304
src/main.ts
Normal file
304
src/main.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
import { Notice, Plugin, TFile, WorkspaceLeaf, normalizePath } from 'obsidian';
|
||||
import { format } from 'date-fns';
|
||||
import * as YAML from 'yaml';
|
||||
import {
|
||||
ChronoSyncSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
ChronoSyncSettingTab
|
||||
} from './settings/settings';
|
||||
import {
|
||||
CALENDAR_VIEW_TYPE,
|
||||
NOTES_VIEW_TYPE,
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
TimeInfo
|
||||
} from './types';
|
||||
import { CalendarView } from './views/CalendarView';
|
||||
import { TaskListView } from './views/TaskListView';
|
||||
import { NotesView } from './views/NotesView';
|
||||
import { TaskCreationModal } from './modals/TaskCreationModal';
|
||||
import {
|
||||
ensureFolderExists,
|
||||
generateDailyNoteTemplate,
|
||||
parseTime,
|
||||
updateYamlFrontmatter
|
||||
} from './utils/helpers';
|
||||
|
||||
export default class ChronoSyncPlugin extends Plugin {
|
||||
settings: ChronoSyncSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Register view types
|
||||
this.registerView(
|
||||
CALENDAR_VIEW_TYPE,
|
||||
(leaf) => new CalendarView(leaf, this)
|
||||
);
|
||||
this.registerView(
|
||||
TASK_LIST_VIEW_TYPE,
|
||||
(leaf) => new TaskListView(leaf, this)
|
||||
);
|
||||
this.registerView(
|
||||
NOTES_VIEW_TYPE,
|
||||
(leaf) => new NotesView(leaf, this)
|
||||
);
|
||||
|
||||
// Add ribbon icon
|
||||
this.addRibbonIcon('calendar-days', 'ChronoSync', async () => {
|
||||
await this.activateCalendarView();
|
||||
});
|
||||
|
||||
// Add commands
|
||||
this.addCommands();
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new ChronoSyncSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Views cleanup happens automatically
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
addCommands() {
|
||||
// Dashboard/Calendar commands
|
||||
this.addCommand({
|
||||
id: 'open-dashboard',
|
||||
name: 'Open dashboard/calendar view',
|
||||
callback: async () => {
|
||||
await this.activateCalendarView();
|
||||
}
|
||||
});
|
||||
|
||||
// Task commands
|
||||
this.addCommand({
|
||||
id: 'create-new-task',
|
||||
name: 'Create new task',
|
||||
callback: () => {
|
||||
this.openTaskCreationModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Note commands
|
||||
this.addCommand({
|
||||
id: 'go-to-today',
|
||||
name: 'Go to today\'s note',
|
||||
callback: async () => {
|
||||
await this.navigateToCurrentDailyNote();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'go-to-home',
|
||||
name: 'Open home note',
|
||||
callback: async () => {
|
||||
await this.navigateToHomeNote();
|
||||
}
|
||||
});
|
||||
|
||||
// Daily note metadata commands
|
||||
this.addCommand({
|
||||
id: 'increment-pomodoros',
|
||||
name: 'Increment daily pomodoros',
|
||||
callback: async () => {
|
||||
await this.incrementPomodoros();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'toggle-workout',
|
||||
name: 'Toggle daily workout',
|
||||
callback: async () => {
|
||||
await this.toggleDailyMetadata('workout');
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'toggle-meditate',
|
||||
name: 'Toggle daily meditation',
|
||||
callback: async () => {
|
||||
await this.toggleDailyMetadata('meditate');
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'toggle-important',
|
||||
name: 'Toggle daily important flag',
|
||||
callback: async () => {
|
||||
await this.toggleDailyMetadata('important');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async activateCalendarView() {
|
||||
const { workspace } = this.app;
|
||||
|
||||
// Close existing calendar view if it exists
|
||||
let leaf = this.getCalendarLeaf();
|
||||
|
||||
if (!leaf) {
|
||||
// Create new leaf for calendar view
|
||||
leaf = workspace.getLeaf('split', 'vertical');
|
||||
await leaf.setViewState({
|
||||
type: CALENDAR_VIEW_TYPE,
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Reveal the leaf in case it's in a collapsed state
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
getCalendarLeaf(): WorkspaceLeaf | null {
|
||||
const { workspace } = this.app;
|
||||
const leaves = workspace.getLeavesOfType(CALENDAR_VIEW_TYPE);
|
||||
return leaves.length > 0 ? leaves[0] : null;
|
||||
}
|
||||
|
||||
async navigateToCurrentDailyNote() {
|
||||
const date = new Date();
|
||||
await this.navigateToDailyNote(date);
|
||||
}
|
||||
|
||||
async navigateToDailyNote(date: Date) {
|
||||
const dailyNoteFileName = format(date, 'yyyy-MM-dd') + '.md';
|
||||
const dailyNotePath = normalizePath(`${this.settings.dailyNotesFolder}/${dailyNoteFileName}`);
|
||||
|
||||
// Check if the daily note exists, if not create it
|
||||
const fileExists = await this.app.vault.adapter.exists(dailyNotePath);
|
||||
|
||||
if (!fileExists) {
|
||||
// Create the daily notes folder if it doesn't exist
|
||||
await ensureFolderExists(this.app.vault, this.settings.dailyNotesFolder);
|
||||
|
||||
// Create daily note with default content
|
||||
const content = this.generateDailyNoteTemplate(date);
|
||||
await this.app.vault.create(dailyNotePath, content);
|
||||
}
|
||||
|
||||
// Open the daily note
|
||||
const file = this.app.vault.getAbstractFileByPath(dailyNotePath);
|
||||
if (file instanceof TFile) {
|
||||
await this.app.workspace.getLeaf(false).openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToHomeNote() {
|
||||
const homeNotePath = this.settings.homeNotePath;
|
||||
|
||||
// Check if the home note exists, if not create it
|
||||
const fileExists = await this.app.vault.adapter.exists(homeNotePath);
|
||||
|
||||
if (!fileExists) {
|
||||
// Create the parent folder if it doesn't exist
|
||||
const folderPath = homeNotePath.substring(0, homeNotePath.lastIndexOf('/'));
|
||||
await ensureFolderExists(this.app.vault, folderPath);
|
||||
|
||||
// Create home note with default content
|
||||
const content = '# ChronoSync Home\n\nWelcome to your ChronoSync Home note!\n';
|
||||
await this.app.vault.create(homeNotePath, content);
|
||||
}
|
||||
|
||||
// Open the home note
|
||||
const file = this.app.vault.getAbstractFileByPath(homeNotePath);
|
||||
if (file instanceof TFile) {
|
||||
await this.app.workspace.getLeaf(false).openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async incrementPomodoros() {
|
||||
await this.updateDailyNoteMetadata('pomodoros', (val) => {
|
||||
const current = typeof val === 'number' ? val : 0;
|
||||
return current + 1;
|
||||
});
|
||||
}
|
||||
|
||||
async toggleDailyMetadata(key: 'workout' | 'meditate' | 'important') {
|
||||
await this.updateDailyNoteMetadata(key, (val) => {
|
||||
return typeof val === 'boolean' ? !val : true;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDailyNoteMetadata(key: string, updateFn: (val: any) => any) {
|
||||
// Get the current daily note file
|
||||
const date = new Date();
|
||||
const dailyNoteFileName = format(date, 'yyyy-MM-dd') + '.md';
|
||||
const dailyNotePath = normalizePath(`${this.settings.dailyNotesFolder}/${dailyNoteFileName}`);
|
||||
|
||||
// Check if the daily note exists, if not create it
|
||||
const fileExists = await this.app.vault.adapter.exists(dailyNotePath);
|
||||
|
||||
if (!fileExists) {
|
||||
await this.navigateToCurrentDailyNote();
|
||||
}
|
||||
|
||||
// Get the file and update its metadata
|
||||
const file = this.app.vault.getAbstractFileByPath(dailyNotePath);
|
||||
if (file instanceof TFile) {
|
||||
try {
|
||||
// Process the frontmatter using FileManager.processFrontMatter for safer modification
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
frontmatter[key] = updateFn(frontmatter[key]);
|
||||
});
|
||||
|
||||
// Show notice
|
||||
new Notice(`Updated ${key} in daily note`);
|
||||
} catch (error) {
|
||||
console.error('Error updating daily note metadata:', error);
|
||||
new Notice('Error updating daily note metadata');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateDailyNoteTemplate(date: Date): string {
|
||||
const startTime = parseTime(this.settings.timeblockStartTime);
|
||||
const endTime = parseTime(this.settings.timeblockEndTime);
|
||||
const intervalMinutes = parseInt(this.settings.timeblockInterval);
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
return 'Error: Invalid timeblock settings';
|
||||
}
|
||||
|
||||
return generateDailyNoteTemplate(
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
intervalMinutes,
|
||||
this.settings.autoAddTimeblock
|
||||
);
|
||||
}
|
||||
|
||||
generateTimeblockTable(): string {
|
||||
// Create a timeblock table based on settings
|
||||
const startTime = parseTime(this.settings.timeblockStartTime);
|
||||
const endTime = parseTime(this.settings.timeblockEndTime);
|
||||
const intervalMinutes = parseInt(this.settings.timeblockInterval);
|
||||
|
||||
if (!startTime || !endTime) return '';
|
||||
|
||||
let table = '| Time | Activity |\n| ---- | -------- |\n';
|
||||
|
||||
const startMinutes = startTime.hours * 60 + startTime.minutes;
|
||||
const endMinutes = endTime.hours * 60 + endTime.minutes;
|
||||
|
||||
for (let minutes = startMinutes; minutes <= endMinutes; minutes += intervalMinutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
||||
table += `| ${timeStr} | |\n`;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
openTaskCreationModal() {
|
||||
new TaskCreationModal(this.app, this).open();
|
||||
}
|
||||
}
|
||||
293
src/modals/TaskCreationModal.ts
Normal file
293
src/modals/TaskCreationModal.ts
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
import { App, Modal, Notice, TFile } from 'obsidian';
|
||||
import { format } from 'date-fns';
|
||||
import * as YAML from 'yaml';
|
||||
import ChronoSyncPlugin from '../main';
|
||||
import { ensureFolderExists } from '../utils/helpers';
|
||||
import { CALENDAR_VIEW_TYPE } from '../types';
|
||||
import { CalendarView } from '../views/CalendarView';
|
||||
|
||||
export class TaskCreationModal extends Modal {
|
||||
plugin: ChronoSyncPlugin;
|
||||
title: string = '';
|
||||
details: string = '';
|
||||
dueDate: string = '';
|
||||
priority: 'low' | 'normal' | 'high' = 'normal';
|
||||
contexts: string = '';
|
||||
tags: string = '';
|
||||
recurrence: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' = 'none';
|
||||
daysOfWeek: string[] = [];
|
||||
dayOfMonth: string = '';
|
||||
|
||||
constructor(app: App, plugin: ChronoSyncPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('task-creation-modal');
|
||||
contentEl.createEl('h2', { text: 'Create new task' });
|
||||
|
||||
// Title
|
||||
this.createFormGroup(contentEl, 'Title', (container) => {
|
||||
const input = container.createEl('input', { type: 'text' });
|
||||
input.addEventListener('input', (e) => {
|
||||
this.title = (e.target as HTMLInputElement).value;
|
||||
});
|
||||
});
|
||||
|
||||
// Details
|
||||
this.createFormGroup(contentEl, 'Details', (container) => {
|
||||
const textarea = container.createEl('textarea');
|
||||
textarea.rows = 3;
|
||||
textarea.addEventListener('input', (e) => {
|
||||
this.details = (e.target as HTMLTextAreaElement).value;
|
||||
});
|
||||
});
|
||||
|
||||
// Due Date
|
||||
this.createFormGroup(contentEl, 'Due date', (container) => {
|
||||
const input = container.createEl('input', { type: 'date' });
|
||||
input.addEventListener('change', (e) => {
|
||||
this.dueDate = (e.target as HTMLInputElement).value;
|
||||
});
|
||||
});
|
||||
|
||||
// Priority
|
||||
this.createFormGroup(contentEl, 'Priority', (container) => {
|
||||
const select = container.createEl('select');
|
||||
|
||||
const options = [
|
||||
{ value: 'low', text: 'Low' },
|
||||
{ value: 'normal', text: 'Normal' },
|
||||
{ value: 'high', text: 'High' }
|
||||
];
|
||||
|
||||
options.forEach(option => {
|
||||
const optEl = select.createEl('option', { value: option.value, text: option.text });
|
||||
if (option.value === this.plugin.settings.defaultTaskPriority) {
|
||||
optEl.selected = true;
|
||||
this.priority = option.value as 'low' | 'normal' | 'high';
|
||||
}
|
||||
});
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
this.priority = (e.target as HTMLSelectElement).value as 'low' | 'normal' | 'high';
|
||||
});
|
||||
});
|
||||
|
||||
// Contexts
|
||||
this.createFormGroup(contentEl, 'Contexts (comma-separated)', (container) => {
|
||||
const input = container.createEl('input', { type: 'text' });
|
||||
input.addEventListener('input', (e) => {
|
||||
this.contexts = (e.target as HTMLInputElement).value;
|
||||
});
|
||||
});
|
||||
|
||||
// Tags
|
||||
this.createFormGroup(contentEl, 'Tags (comma-separated)', (container) => {
|
||||
const input = container.createEl('input', { type: 'text' });
|
||||
input.addEventListener('input', (e) => {
|
||||
this.tags = (e.target as HTMLInputElement).value;
|
||||
});
|
||||
});
|
||||
|
||||
// Recurrence
|
||||
this.createFormGroup(contentEl, 'Recurrence', (container) => {
|
||||
const select = container.createEl('select');
|
||||
|
||||
const options = [
|
||||
{ value: 'none', text: 'None' },
|
||||
{ value: 'daily', text: 'Daily' },
|
||||
{ value: 'weekly', text: 'Weekly' },
|
||||
{ value: 'monthly', text: 'Monthly' },
|
||||
{ value: 'yearly', text: 'Yearly' }
|
||||
];
|
||||
|
||||
options.forEach(option => {
|
||||
select.createEl('option', { value: option.value, text: option.text });
|
||||
});
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
this.recurrence = (e.target as HTMLSelectElement).value as any;
|
||||
this.updateRecurrenceOptions(contentEl);
|
||||
});
|
||||
});
|
||||
|
||||
// The recurrence options container (will be populated based on recurrence selection)
|
||||
const recurrenceOptions = contentEl.createDiv({ cls: 'recurrence-options' });
|
||||
(recurrenceOptions as HTMLElement).style.display = 'none';
|
||||
|
||||
// Buttons
|
||||
const buttonContainer = contentEl.createDiv({ cls: 'button-container' });
|
||||
|
||||
const cancelButton = buttonContainer.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
const createButton = buttonContainer.createEl('button', { text: 'Create task', cls: 'create-button' });
|
||||
createButton.addEventListener('click', () => {
|
||||
this.createTask();
|
||||
});
|
||||
}
|
||||
|
||||
createFormGroup(container: HTMLElement, label: string, inputCallback: (container: HTMLElement) => void) {
|
||||
const group = container.createDiv({ cls: 'form-group' });
|
||||
group.createEl('label', { text: label });
|
||||
inputCallback(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
updateRecurrenceOptions(container: HTMLElement) {
|
||||
const optionsContainer = container.querySelector('.recurrence-options');
|
||||
if (!optionsContainer) return;
|
||||
|
||||
optionsContainer.empty();
|
||||
(optionsContainer as HTMLElement).style.display = 'block';
|
||||
|
||||
if (this.recurrence === 'weekly') {
|
||||
this.createDaysOfWeekSelector(optionsContainer as HTMLElement);
|
||||
} else if (this.recurrence === 'monthly' || this.recurrence === 'yearly') {
|
||||
this.createDayOfMonthSelector(optionsContainer as HTMLElement);
|
||||
} else {
|
||||
(optionsContainer as HTMLElement).style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
createDaysOfWeekSelector(container: HTMLElement) {
|
||||
container.createEl('label', { text: 'Days of week' });
|
||||
const checkboxContainer = container.createDiv({ cls: 'checkbox-container' });
|
||||
|
||||
const daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
const shortDays = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
||||
|
||||
daysOfWeek.forEach((day, index) => {
|
||||
const label = checkboxContainer.createEl('label', { cls: 'checkbox-label' });
|
||||
const checkbox = label.createEl('input', { type: 'checkbox' });
|
||||
checkbox.dataset.day = shortDays[index];
|
||||
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const isChecked = (e.target as HTMLInputElement).checked;
|
||||
const day = (e.target as HTMLInputElement).dataset.day;
|
||||
|
||||
if (isChecked && day) {
|
||||
this.daysOfWeek.push(day);
|
||||
} else if (day) {
|
||||
this.daysOfWeek = this.daysOfWeek.filter(d => d !== day);
|
||||
}
|
||||
});
|
||||
|
||||
label.appendChild(document.createTextNode(day));
|
||||
});
|
||||
}
|
||||
|
||||
createDayOfMonthSelector(container: HTMLElement) {
|
||||
this.createFormGroup(container, 'Day of month', (group) => {
|
||||
const input = group.createEl('input', { type: 'number' });
|
||||
input.min = '1';
|
||||
input.max = '31';
|
||||
input.addEventListener('input', (e) => {
|
||||
this.dayOfMonth = (e.target as HTMLInputElement).value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async createTask() {
|
||||
if (!this.title) {
|
||||
new Notice('Title is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique ID for the task
|
||||
const now = new Date();
|
||||
const datePart = format(now, 'yyyyMMdd');
|
||||
const randomPart = Math.random().toString(36).substring(2, 5);
|
||||
const taskId = `${datePart}${randomPart}`;
|
||||
|
||||
// Ensure the tasks folder exists
|
||||
await ensureFolderExists(this.app.vault, this.plugin.settings.tasksFolder);
|
||||
|
||||
// Create the task file
|
||||
const taskFilePath = `${this.plugin.settings.tasksFolder}/${taskId}.md`;
|
||||
|
||||
// Prepare tags (always include "task")
|
||||
let tagsArray = ['task'];
|
||||
if (this.tags) {
|
||||
tagsArray = tagsArray.concat(this.tags.split(',').map(tag => tag.trim()));
|
||||
}
|
||||
|
||||
// Prepare contexts
|
||||
let contextsArray: string[] = [];
|
||||
if (this.contexts) {
|
||||
contextsArray = this.contexts.split(',').map(context => context.trim());
|
||||
}
|
||||
|
||||
// Create the YAML frontmatter
|
||||
const yaml: any = {
|
||||
title: this.title,
|
||||
zettelid: taskId,
|
||||
dateCreated: format(now, "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
dateModified: format(now, "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
status: this.plugin.settings.defaultTaskStatus,
|
||||
tags: tagsArray,
|
||||
priority: this.priority,
|
||||
};
|
||||
|
||||
// Add optional fields
|
||||
if (this.dueDate) {
|
||||
yaml.due = this.dueDate;
|
||||
}
|
||||
|
||||
if (contextsArray.length > 0) {
|
||||
yaml.contexts = contextsArray;
|
||||
}
|
||||
|
||||
// Add recurrence info if specified
|
||||
if (this.recurrence !== 'none') {
|
||||
yaml.recurrence = {
|
||||
frequency: this.recurrence
|
||||
};
|
||||
|
||||
if (this.recurrence === 'weekly' && this.daysOfWeek.length > 0) {
|
||||
yaml.recurrence.days_of_week = this.daysOfWeek;
|
||||
}
|
||||
|
||||
if ((this.recurrence === 'monthly' || this.recurrence === 'yearly') && this.dayOfMonth) {
|
||||
yaml.recurrence.day_of_month = parseInt(this.dayOfMonth);
|
||||
}
|
||||
|
||||
yaml.complete_instances = [];
|
||||
}
|
||||
|
||||
// Prepare the file content
|
||||
const content = `---\n${YAML.stringify(yaml)}---\n\n# ${this.title}\n\n${this.details}`;
|
||||
|
||||
// Create the file
|
||||
await this.app.vault.create(taskFilePath, content);
|
||||
|
||||
// Show success notice and close modal
|
||||
new Notice('Task created successfully');
|
||||
this.close();
|
||||
|
||||
// Find calendar view and refresh it if open
|
||||
const calendarLeaf = this.plugin.getCalendarLeaf();
|
||||
if (calendarLeaf) {
|
||||
const view = calendarLeaf.view as CalendarView;
|
||||
if (view.activeTab === 'tasks') {
|
||||
// Refresh the view
|
||||
view.refreshView();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
new Notice('Error creating task. Check the console for details.');
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
163
src/settings/settings.ts
Normal file
163
src/settings/settings.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import ChronoSyncPlugin from '../main';
|
||||
|
||||
export interface ChronoSyncSettings {
|
||||
dailyNotesFolder: string;
|
||||
tasksFolder: string;
|
||||
notesFolder: string;
|
||||
homeNotePath: string;
|
||||
defaultTaskPriority: 'low' | 'normal' | 'high';
|
||||
defaultTaskStatus: 'open' | 'in-progress' | 'done';
|
||||
timeblockStartTime: string;
|
||||
timeblockEndTime: string;
|
||||
timeblockInterval: '30' | '60';
|
||||
autoAddTimeblock: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ChronoSyncSettings = {
|
||||
dailyNotesFolder: 'ChronoSync/Daily',
|
||||
tasksFolder: 'ChronoSync/Tasks',
|
||||
notesFolder: 'ChronoSync/Notes',
|
||||
homeNotePath: 'ChronoSync/Home.md',
|
||||
defaultTaskPriority: 'normal',
|
||||
defaultTaskStatus: 'open',
|
||||
timeblockStartTime: '05:00',
|
||||
timeblockEndTime: '23:30',
|
||||
timeblockInterval: '30',
|
||||
autoAddTimeblock: true
|
||||
};
|
||||
|
||||
export class ChronoSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ChronoSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: ChronoSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// General Settings
|
||||
new Setting(containerEl)
|
||||
.setName('Daily notes folder')
|
||||
.setDesc('Folder where daily notes will be stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ChronoSync/Daily')
|
||||
.setValue(this.plugin.settings.dailyNotesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dailyNotesFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tasks folder')
|
||||
.setDesc('Folder where tasks will be stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ChronoSync/Tasks')
|
||||
.setValue(this.plugin.settings.tasksFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.tasksFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Notes folder')
|
||||
.setDesc('Folder where general notes will be stored (use "/" for vault root)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ChronoSync/Notes')
|
||||
.setValue(this.plugin.settings.notesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.notesFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Home note path')
|
||||
.setDesc('Path to the home note file')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ChronoSync/Home.md')
|
||||
.setValue(this.plugin.settings.homeNotePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.homeNotePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Task Settings
|
||||
new Setting(containerEl).setName('Tasks').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default task priority')
|
||||
.setDesc('Default priority for new tasks')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('low', 'Low')
|
||||
.addOption('normal', 'Normal')
|
||||
.addOption('high', 'High')
|
||||
.setValue(this.plugin.settings.defaultTaskPriority)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskPriority = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default task status')
|
||||
.setDesc('Default status for new tasks')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('open', 'Open')
|
||||
.addOption('in-progress', 'In Progress')
|
||||
.addOption('done', 'Done')
|
||||
.setValue(this.plugin.settings.defaultTaskStatus)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskStatus = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Timeblock Settings
|
||||
new Setting(containerEl).setName('Timeblocks').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default timeblock start time')
|
||||
.setDesc('Start time for timeblock table (HH:MM format)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('05:00')
|
||||
.setValue(this.plugin.settings.timeblockStartTime)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.timeblockStartTime = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default timeblock end time')
|
||||
.setDesc('End time for timeblock table (HH:MM format)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('23:30')
|
||||
.setValue(this.plugin.settings.timeblockEndTime)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.timeblockEndTime = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Timeblock interval')
|
||||
.setDesc('Interval between timeblock entries')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('30', '30 minutes')
|
||||
.addOption('60', '1 hour')
|
||||
.setValue(this.plugin.settings.timeblockInterval)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.timeblockInterval = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-add timeblock')
|
||||
.setDesc('Automatically add timeblock table to new daily notes')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoAddTimeblock)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoAddTimeblock = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
27
src/types.ts
Normal file
27
src/types.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// View types
|
||||
export const CALENDAR_VIEW_TYPE = 'chronosync-calendar-view';
|
||||
export const TASK_LIST_VIEW_TYPE = 'chronosync-task-list-view';
|
||||
export const NOTES_VIEW_TYPE = 'chronosync-notes-view';
|
||||
|
||||
// Time and date related types
|
||||
export interface TimeInfo {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
}
|
||||
|
||||
// Task types
|
||||
export interface TaskInfo {
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
due?: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
// Note types
|
||||
export interface NoteInfo {
|
||||
title: string;
|
||||
tags: string[];
|
||||
path: string;
|
||||
createdDate?: string;
|
||||
}
|
||||
251
src/utils/helpers.ts
Normal file
251
src/utils/helpers.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { normalizePath, TFile, Vault } from 'obsidian';
|
||||
import { format } from 'date-fns';
|
||||
import { TimeInfo } from '../types';
|
||||
import * as YAML from 'yaml';
|
||||
|
||||
/**
|
||||
* Ensures a folder and its parent folders exist
|
||||
*/
|
||||
export async function ensureFolderExists(vault: Vault, folderPath: string): Promise<void> {
|
||||
const folders = folderPath.split('/').filter(folder => folder.length > 0);
|
||||
let currentPath = '';
|
||||
|
||||
for (const folder of folders) {
|
||||
currentPath = currentPath ? `${currentPath}/${folder}` : folder;
|
||||
const exists = await vault.adapter.exists(currentPath);
|
||||
if (!exists) {
|
||||
await vault.createFolder(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time string in the format HH:MM and returns hours and minutes
|
||||
*/
|
||||
export function parseTime(timeStr: string): TimeInfo | null {
|
||||
const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!match) return null;
|
||||
|
||||
const hours = parseInt(match[1]);
|
||||
const minutes = parseInt(match[2]);
|
||||
|
||||
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { hours, minutes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a YAML frontmatter value in a Markdown file
|
||||
*/
|
||||
export function updateYamlFrontmatter(content: string, key: string, updateFn: (val: any) => any): string {
|
||||
// Check if the content has YAML frontmatter
|
||||
if (!content.startsWith('---')) {
|
||||
// If not, add it with the updated key
|
||||
const yamlObj: any = {};
|
||||
yamlObj[key] = updateFn(undefined);
|
||||
const yamlStr = YAML.stringify(yamlObj);
|
||||
return `---\n${yamlStr}---\n\n${content}`;
|
||||
}
|
||||
|
||||
// Find the end of the frontmatter
|
||||
const endOfFrontmatter = content.indexOf('---', 3);
|
||||
if (endOfFrontmatter === -1) return content;
|
||||
|
||||
// Extract the frontmatter
|
||||
const frontmatter = content.substring(3, endOfFrontmatter);
|
||||
const restOfContent = content.substring(endOfFrontmatter);
|
||||
|
||||
// Parse the frontmatter
|
||||
let yamlObj: any = {};
|
||||
try {
|
||||
yamlObj = YAML.parse(frontmatter) || {};
|
||||
} catch (e) {
|
||||
console.error('Error parsing YAML frontmatter:', e);
|
||||
return content;
|
||||
}
|
||||
|
||||
// Update the key
|
||||
yamlObj[key] = updateFn(yamlObj[key]);
|
||||
|
||||
// Stringify the frontmatter
|
||||
const updatedFrontmatter = YAML.stringify(yamlObj);
|
||||
|
||||
// Return the updated content
|
||||
return `---\n${updatedFrontmatter}---${restOfContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a timeblock table based on start/end times and interval
|
||||
*/
|
||||
export function generateTimeblockTable(startTime: TimeInfo, endTime: TimeInfo, intervalMinutes: number): string {
|
||||
if (!startTime || !endTime) return '';
|
||||
|
||||
let table = '| Time | Activity |\n| ---- | -------- |\n';
|
||||
|
||||
const startMinutes = startTime.hours * 60 + startTime.minutes;
|
||||
const endMinutes = endTime.hours * 60 + endTime.minutes;
|
||||
|
||||
for (let minutes = startMinutes; minutes <= endMinutes; minutes += intervalMinutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
||||
table += `| ${timeStr} | |\n`;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a daily note template
|
||||
*/
|
||||
export function generateDailyNoteTemplate(date: Date, timeblockStartTime: TimeInfo, timeblockEndTime: TimeInfo, intervalMinutes: number, addTimeblock: boolean): string {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
|
||||
// Create the YAML frontmatter
|
||||
const yaml = {
|
||||
date: dateStr,
|
||||
pomodoros: 0,
|
||||
workout: false,
|
||||
meditate: false,
|
||||
important: false,
|
||||
tags: ['daily']
|
||||
};
|
||||
|
||||
let content = `---\n${YAML.stringify(yaml)}---\n\n# ${format(date, 'eeee, MMMM do, yyyy')}\n\n## Notes\n\n`;
|
||||
|
||||
// Add timeblock table if configured
|
||||
if (addTimeblock) {
|
||||
content += `\n## Timeblock\n\n${generateTimeblockTable(timeblockStartTime, timeblockEndTime, parseInt(intervalMinutes.toString()))}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two dates are the same day
|
||||
*/
|
||||
export function isSameDay(date1: Date, date2: Date): boolean {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts task information from a task file's content
|
||||
*/
|
||||
export function extractTaskInfo(content: string, path: string): { title: string, status: string, priority: string, due?: string, path: string } | null {
|
||||
// Try to extract task info from frontmatter
|
||||
if (content.startsWith('---')) {
|
||||
const endOfFrontmatter = content.indexOf('---', 3);
|
||||
if (endOfFrontmatter !== -1) {
|
||||
const frontmatter = content.substring(3, endOfFrontmatter);
|
||||
try {
|
||||
const yaml = YAML.parse(frontmatter);
|
||||
|
||||
if (yaml) {
|
||||
return {
|
||||
title: yaml.title || 'Untitled Task',
|
||||
status: yaml.status || 'open',
|
||||
priority: yaml.priority || 'normal',
|
||||
due: yaml.due,
|
||||
path
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing YAML frontmatter:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to basic info from filename
|
||||
const filename = path.split('/').pop()?.replace('.md', '') || 'Untitled';
|
||||
return {
|
||||
title: filename,
|
||||
status: 'open',
|
||||
priority: 'normal',
|
||||
path
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a task is overdue
|
||||
*/
|
||||
export function isTaskOverdue(task: {due?: string}): boolean {
|
||||
if (!task.due) return false;
|
||||
|
||||
const dueDate = new Date(task.due);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
return dueDate < today;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts note information from a note file's content
|
||||
*/
|
||||
export function extractNoteInfo(content: string, path: string, file?: TFile): {title: string, tags: string[], path: string, createdDate?: string} | null {
|
||||
let title = path.split('/').pop()?.replace('.md', '') || 'Untitled';
|
||||
let tags: string[] = [];
|
||||
let createdDate: string | undefined = undefined;
|
||||
|
||||
// Try to extract note info from frontmatter
|
||||
if (content.startsWith('---')) {
|
||||
const endOfFrontmatter = content.indexOf('---', 3);
|
||||
if (endOfFrontmatter !== -1) {
|
||||
const frontmatter = content.substring(3, endOfFrontmatter);
|
||||
try {
|
||||
const yaml = YAML.parse(frontmatter);
|
||||
|
||||
if (yaml) {
|
||||
if (yaml.title) {
|
||||
title = yaml.title;
|
||||
}
|
||||
|
||||
if (yaml.tags && Array.isArray(yaml.tags)) {
|
||||
tags = yaml.tags;
|
||||
}
|
||||
|
||||
// Extract creation date from dateCreated or date field
|
||||
if (yaml.dateCreated) {
|
||||
createdDate = yaml.dateCreated;
|
||||
} else if (yaml.date) {
|
||||
createdDate = yaml.date;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing YAML frontmatter:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for first heading in the content as a fallback title
|
||||
if (title === 'Untitled') {
|
||||
const headingMatch = content.match(/^#\s+(.+)$/m);
|
||||
if (headingMatch && headingMatch[1]) {
|
||||
title = headingMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// If no creation date in frontmatter, use file creation time
|
||||
if (!createdDate && file) {
|
||||
createdDate = format(new Date(file.stat.ctime), "yyyy-MM-dd'T'HH:mm:ss");
|
||||
}
|
||||
|
||||
return { title, tags, path, createdDate };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts timeblock content from a daily note
|
||||
*/
|
||||
export function extractTimeblockContent(content: string): string | null {
|
||||
// Check if the content has a timeblock section
|
||||
const timeblockMatch = content.match(/## Timeblock\s*\n([^#]*)/);
|
||||
|
||||
if (timeblockMatch && timeblockMatch[1]) {
|
||||
return timeblockMatch[1].trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
895
src/views/CalendarView.ts
Normal file
895
src/views/CalendarView.ts
Normal file
|
|
@ -0,0 +1,895 @@
|
|||
import { Notice, TFile, View, WorkspaceLeaf, normalizePath } from 'obsidian';
|
||||
import { format } from 'date-fns';
|
||||
import ChronoSyncPlugin from '../main';
|
||||
import { CALENDAR_VIEW_TYPE, TaskInfo, NoteInfo, TimeInfo } from '../types';
|
||||
import {
|
||||
extractNoteInfo,
|
||||
extractTaskInfo,
|
||||
extractTimeblockContent,
|
||||
isTaskOverdue,
|
||||
isSameDay,
|
||||
parseTime
|
||||
} from '../utils/helpers';
|
||||
|
||||
export class CalendarView extends View {
|
||||
plugin: ChronoSyncPlugin;
|
||||
currentDate: Date;
|
||||
viewType: 'month' | 'week' = 'month';
|
||||
activeTab: 'tasks' | 'notes' | 'timeblock' = 'tasks';
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ChronoSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.currentDate = new Date();
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return CALENDAR_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Calendar';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'calendar-days';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
// Clear and prepare the content element
|
||||
const contentEl = this.containerEl;
|
||||
contentEl.empty();
|
||||
|
||||
// Add a container for our view content
|
||||
const container = contentEl.createDiv({ cls: 'chronosync-container' });
|
||||
|
||||
// Create and add UI elements
|
||||
container.createEl('h2', { text: 'ChronoSync Calendar' });
|
||||
|
||||
// Create the calendar UI
|
||||
this.renderView(container);
|
||||
}
|
||||
|
||||
renderView(container: HTMLElement) {
|
||||
// Clear existing content except the header
|
||||
const header = container.querySelector('h2');
|
||||
container.empty();
|
||||
if (header) container.appendChild(header);
|
||||
|
||||
// Create the calendar UI
|
||||
this.createCalendarControls(container);
|
||||
|
||||
if (this.viewType === 'month') {
|
||||
this.createCalendarGrid(container);
|
||||
} else {
|
||||
this.createWeekView(container);
|
||||
}
|
||||
|
||||
this.createSidePanel(container);
|
||||
}
|
||||
|
||||
navigateToPreviousPeriod() {
|
||||
if (this.viewType === 'month') {
|
||||
// Go to previous month
|
||||
this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1);
|
||||
} else {
|
||||
// Go to previous week
|
||||
this.currentDate = new Date(this.currentDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
navigateToNextPeriod() {
|
||||
if (this.viewType === 'month') {
|
||||
// Go to next month
|
||||
this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1);
|
||||
} else {
|
||||
// Go to next week
|
||||
this.currentDate = new Date(this.currentDate.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
navigateToToday() {
|
||||
this.currentDate = new Date();
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
switchView(viewType: 'month' | 'week') {
|
||||
if (this.viewType === viewType) return;
|
||||
this.viewType = viewType;
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
// Clean up when the view is closed
|
||||
this.containerEl.empty();
|
||||
}
|
||||
|
||||
// Helper method to refresh the view
|
||||
refreshView() {
|
||||
// Store the current active tab
|
||||
const activeTabBefore = this.activeTab;
|
||||
|
||||
const container = this.containerEl.querySelector('.chronosync-container') as HTMLElement;
|
||||
if (container) {
|
||||
this.renderView(container);
|
||||
|
||||
// Restore the active tab
|
||||
if (activeTabBefore) {
|
||||
// Find the tab and click it
|
||||
const tabSelector = `.chronosync-tab[data-tab="${activeTabBefore}"]`;
|
||||
const tabButton = container.querySelector(tabSelector);
|
||||
if (tabButton) {
|
||||
(tabButton as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createCalendarControls(container: HTMLElement) {
|
||||
const controlsContainer = container.createDiv({ cls: 'calendar-controls' });
|
||||
|
||||
// Add month navigation
|
||||
const navContainer = controlsContainer.createDiv({ cls: 'calendar-nav' });
|
||||
|
||||
const prevButton = navContainer.createEl('button', { text: '←' });
|
||||
prevButton.addEventListener('click', () => {
|
||||
this.navigateToPreviousPeriod();
|
||||
});
|
||||
|
||||
const currentMonth = navContainer.createEl('span', {
|
||||
text: format(this.currentDate, 'MMMM yyyy'),
|
||||
cls: 'current-month'
|
||||
});
|
||||
|
||||
const todayButton = navContainer.createEl('button', { text: 'Today' });
|
||||
todayButton.addEventListener('click', () => {
|
||||
this.navigateToToday();
|
||||
});
|
||||
|
||||
const nextButton = navContainer.createEl('button', { text: '→' });
|
||||
nextButton.addEventListener('click', () => {
|
||||
this.navigateToNextPeriod();
|
||||
});
|
||||
|
||||
// Add view type switcher
|
||||
const viewContainer = controlsContainer.createDiv({ cls: 'view-switcher' });
|
||||
|
||||
const monthButton = viewContainer.createEl('button', { text: 'Month' });
|
||||
if (this.viewType === 'month') monthButton.classList.add('active');
|
||||
monthButton.addEventListener('click', () => {
|
||||
this.switchView('month');
|
||||
});
|
||||
|
||||
const weekButton = viewContainer.createEl('button', { text: 'Week' });
|
||||
if (this.viewType === 'week') weekButton.classList.add('active');
|
||||
weekButton.addEventListener('click', () => {
|
||||
this.switchView('week');
|
||||
});
|
||||
}
|
||||
|
||||
createCalendarGrid(container: HTMLElement) {
|
||||
const calendarContainer = container.createDiv({ cls: 'calendar-grid-container' });
|
||||
const grid = calendarContainer.createDiv({ cls: 'calendar-grid' });
|
||||
|
||||
// Create header with day names
|
||||
const header = grid.createDiv({ cls: 'calendar-header' });
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
for (const day of dayNames) {
|
||||
header.createDiv({ cls: 'calendar-day-header', text: day });
|
||||
}
|
||||
|
||||
// Get current year and month from the currentDate
|
||||
const currentYear = this.currentDate.getFullYear();
|
||||
const currentMonth = this.currentDate.getMonth();
|
||||
|
||||
// First day of the month
|
||||
const firstDay = new Date(currentYear, currentMonth, 1);
|
||||
// Last day of the month
|
||||
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
||||
|
||||
// Get the day of the week for the first day (0 = Sunday, 6 = Saturday)
|
||||
const firstDayOfWeek = firstDay.getDay();
|
||||
|
||||
// Calculate the total number of cells needed (max 6 rows of 7 days)
|
||||
const totalDays = lastDay.getDate();
|
||||
const totalCells = Math.ceil((totalDays + firstDayOfWeek) / 7) * 7;
|
||||
|
||||
// Create the grid cells
|
||||
for (let i = 0; i < totalCells; i++) {
|
||||
const dayCell = grid.createDiv({ cls: 'calendar-day' });
|
||||
|
||||
// Calculate the day number
|
||||
const dayNumber = i - firstDayOfWeek + 1;
|
||||
|
||||
if (dayNumber > 0 && dayNumber <= totalDays) {
|
||||
// Regular day in current month
|
||||
dayCell.textContent = dayNumber.toString();
|
||||
|
||||
const cellDate = new Date(currentYear, currentMonth, dayNumber);
|
||||
|
||||
// Highlight today
|
||||
const today = new Date();
|
||||
if (today.getFullYear() === cellDate.getFullYear() &&
|
||||
today.getMonth() === cellDate.getMonth() &&
|
||||
today.getDate() === cellDate.getDate()) {
|
||||
dayCell.classList.add('today');
|
||||
}
|
||||
|
||||
// Highlight selected date
|
||||
if (isSameDay(cellDate, this.currentDate)) {
|
||||
dayCell.classList.add('selected');
|
||||
}
|
||||
|
||||
// Add click event handler - just select the date, don't open the note
|
||||
dayCell.addEventListener('click', () => {
|
||||
// Update current date and refresh the view
|
||||
this.currentDate = new Date(cellDate);
|
||||
this.refreshView();
|
||||
});
|
||||
|
||||
// Add double-click handler to open the daily note
|
||||
dayCell.addEventListener('dblclick', () => {
|
||||
// Navigate to daily note for this day
|
||||
this.navigateToDailyNote(cellDate);
|
||||
});
|
||||
|
||||
|
||||
// TODO: Add indicators for tasks, daily notes, etc.
|
||||
} else {
|
||||
// Day outside current month
|
||||
dayCell.classList.add('outside-month');
|
||||
|
||||
// Calculate the actual date for days outside current month
|
||||
let date: Date;
|
||||
if (dayNumber <= 0) {
|
||||
// Days from previous month
|
||||
const prevMonth = new Date(currentYear, currentMonth - 1, 1);
|
||||
const daysInPrevMonth = new Date(currentYear, currentMonth, 0).getDate();
|
||||
date = new Date(prevMonth.getFullYear(), prevMonth.getMonth(), daysInPrevMonth + dayNumber);
|
||||
dayCell.textContent = date.getDate().toString();
|
||||
} else {
|
||||
// Days from next month
|
||||
const nextMonth = new Date(currentYear, currentMonth + 1, 1);
|
||||
date = new Date(nextMonth.getFullYear(), nextMonth.getMonth(), dayNumber - totalDays);
|
||||
dayCell.textContent = date.getDate().toString();
|
||||
}
|
||||
|
||||
// Highlight selected date
|
||||
if (isSameDay(date, this.currentDate)) {
|
||||
dayCell.classList.add('selected');
|
||||
}
|
||||
|
||||
// Add click event handler for days outside current month - just select the date
|
||||
dayCell.addEventListener('click', () => {
|
||||
// Update current date and refresh the view
|
||||
this.currentDate = new Date(date);
|
||||
this.refreshView();
|
||||
});
|
||||
|
||||
// Add double-click handler to open the daily note
|
||||
dayCell.addEventListener('dblclick', () => {
|
||||
// Navigate to daily note for this day
|
||||
this.navigateToDailyNote(date);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createWeekView(container: HTMLElement) {
|
||||
const weekContainer = container.createDiv({ cls: 'week-view-container' });
|
||||
const grid = weekContainer.createDiv({ cls: 'week-grid' });
|
||||
|
||||
// Determine the start of the week (Sunday of the week containing currentDate)
|
||||
const currentDate = new Date(this.currentDate);
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
const startOfWeek = new Date(currentDate);
|
||||
startOfWeek.setDate(currentDate.getDate() - dayOfWeek);
|
||||
|
||||
// Create header with day names and dates
|
||||
const header = grid.createDiv({ cls: 'week-header' });
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(startOfWeek);
|
||||
date.setDate(startOfWeek.getDate() + i);
|
||||
|
||||
const dayHeader = header.createDiv({ cls: 'week-day-header' });
|
||||
dayHeader.createDiv({ cls: 'week-day-name', text: dayNames[i] });
|
||||
dayHeader.createDiv({ cls: 'week-day-date', text: format(date, 'MMM d') });
|
||||
|
||||
// Highlight today
|
||||
const today = new Date();
|
||||
if (today.getFullYear() === date.getFullYear() &&
|
||||
today.getMonth() === date.getMonth() &&
|
||||
today.getDate() === date.getDate()) {
|
||||
dayHeader.classList.add('today');
|
||||
}
|
||||
|
||||
// Highlight selected date
|
||||
if (isSameDay(date, this.currentDate)) {
|
||||
dayHeader.classList.add('selected');
|
||||
}
|
||||
|
||||
// Add click event to select the day
|
||||
dayHeader.addEventListener('click', () => {
|
||||
this.currentDate = new Date(date);
|
||||
this.refreshView();
|
||||
});
|
||||
|
||||
// Add double-click event to navigate to the daily note
|
||||
dayHeader.addEventListener('dblclick', () => {
|
||||
this.navigateToDailyNote(date);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Create time slots for the week view
|
||||
const timeContainer = grid.createDiv({ cls: 'week-time-container' });
|
||||
|
||||
// Start and end times based on settings
|
||||
const startTime = parseTime(this.plugin.settings.timeblockStartTime);
|
||||
const endTime = parseTime(this.plugin.settings.timeblockEndTime);
|
||||
const intervalMinutes = parseInt(this.plugin.settings.timeblockInterval);
|
||||
|
||||
if (!startTime || !endTime) return;
|
||||
|
||||
const startMinutes = startTime.hours * 60 + startTime.minutes;
|
||||
const endMinutes = endTime.hours * 60 + endTime.minutes;
|
||||
|
||||
for (let minutes = startMinutes; minutes <= endMinutes; minutes += intervalMinutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
||||
|
||||
const timeRow = timeContainer.createDiv({ cls: 'week-time-row' });
|
||||
timeRow.createDiv({ cls: 'week-time-label', text: timeStr });
|
||||
|
||||
const timeGrid = timeRow.createDiv({ cls: 'week-time-grid' });
|
||||
|
||||
// Create a cell for each day in the week
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(startOfWeek);
|
||||
date.setDate(startOfWeek.getDate() + i);
|
||||
|
||||
const cell = timeGrid.createDiv({ cls: 'week-time-cell' });
|
||||
|
||||
// Add highlight for current time if it matches
|
||||
const now = new Date();
|
||||
if (now.getFullYear() === date.getFullYear() &&
|
||||
now.getMonth() === date.getMonth() &&
|
||||
now.getDate() === date.getDate() &&
|
||||
now.getHours() === hours &&
|
||||
Math.floor(now.getMinutes() / intervalMinutes) * intervalMinutes === mins) {
|
||||
cell.classList.add('current-time');
|
||||
}
|
||||
|
||||
// Add click handler to edit the timeblock
|
||||
cell.addEventListener('click', () => {
|
||||
// TODO: Implement timeblock editing
|
||||
new Notice(`Editing timeblock for ${format(date, 'yyyy-MM-dd')} at ${timeStr}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createSidePanel(container: HTMLElement) {
|
||||
const mainContent = container.createDiv({ cls: 'chronosync-main-content' });
|
||||
|
||||
// Create tabs for different views
|
||||
const tabsContainer = mainContent.createDiv({ cls: 'chronosync-tabs' });
|
||||
|
||||
const tasksTab = tabsContainer.createEl('button', {
|
||||
text: 'Tasks',
|
||||
cls: 'chronosync-tab active',
|
||||
attr: { 'data-tab': 'tasks' }
|
||||
});
|
||||
const notesTab = tabsContainer.createEl('button', {
|
||||
text: 'Notes',
|
||||
cls: 'chronosync-tab',
|
||||
attr: { 'data-tab': 'notes' }
|
||||
});
|
||||
const timeblockTab = tabsContainer.createEl('button', {
|
||||
text: 'Timeblock',
|
||||
cls: 'chronosync-tab',
|
||||
attr: { 'data-tab': 'timeblock' }
|
||||
});
|
||||
|
||||
// Create content area
|
||||
const contentArea = mainContent.createDiv({ cls: 'chronosync-content-area' });
|
||||
|
||||
// Initial content - Tasks view
|
||||
this.createTasksView(contentArea);
|
||||
|
||||
// Store active tab reference
|
||||
this.activeTab = 'tasks';
|
||||
|
||||
// Set up tab switching
|
||||
tasksTab.addEventListener('click', () => {
|
||||
this.setActiveTab(tabsContainer, tasksTab);
|
||||
contentArea.empty();
|
||||
this.activeTab = 'tasks';
|
||||
this.createTasksView(contentArea);
|
||||
});
|
||||
|
||||
notesTab.addEventListener('click', () => {
|
||||
this.setActiveTab(tabsContainer, notesTab);
|
||||
contentArea.empty();
|
||||
this.activeTab = 'notes';
|
||||
this.createNotesView(contentArea);
|
||||
});
|
||||
|
||||
timeblockTab.addEventListener('click', () => {
|
||||
this.setActiveTab(tabsContainer, timeblockTab);
|
||||
contentArea.empty();
|
||||
this.activeTab = 'timeblock';
|
||||
this.createTimeblockView(contentArea);
|
||||
});
|
||||
}
|
||||
|
||||
setActiveTab(container: HTMLElement, activeTab: HTMLElement) {
|
||||
const tabs = container.querySelectorAll('.chronosync-tab');
|
||||
tabs.forEach(tab => tab.classList.remove('active'));
|
||||
activeTab.classList.add('active');
|
||||
}
|
||||
|
||||
async createTasksView(container: HTMLElement) {
|
||||
container.createEl('h3', { text: 'Tasks' });
|
||||
|
||||
// Create filters
|
||||
const filtersContainer = container.createDiv({ cls: 'task-filters' });
|
||||
|
||||
// Status filter
|
||||
const statusFilter = filtersContainer.createDiv({ cls: 'filter-group' });
|
||||
statusFilter.createEl('span', { text: 'Status: ' });
|
||||
const statusSelect = statusFilter.createEl('select');
|
||||
|
||||
const statuses = ['All', 'Open', 'In Progress', 'Done'];
|
||||
statuses.forEach(status => {
|
||||
const option = statusSelect.createEl('option', { value: status.toLowerCase(), text: status });
|
||||
});
|
||||
|
||||
// Add task button
|
||||
const addButton = filtersContainer.createEl('button', { text: 'New Task', cls: 'add-task-button' });
|
||||
addButton.addEventListener('click', () => {
|
||||
// Use the TaskCreationModal from the plugin
|
||||
this.plugin.openTaskCreationModal();
|
||||
});
|
||||
|
||||
// Task list
|
||||
const taskList = container.createDiv({ cls: 'task-list' });
|
||||
|
||||
// Get tasks for the current view
|
||||
const tasks = await this.getTasksForView();
|
||||
|
||||
if (tasks.length === 0) {
|
||||
// Placeholder for empty task list
|
||||
taskList.createEl('p', { text: 'No tasks found for the selected date and filters.' });
|
||||
} else {
|
||||
// Create task items
|
||||
tasks.forEach(task => {
|
||||
const taskItem = taskList.createDiv({ cls: 'task-item' });
|
||||
|
||||
const taskInfo = taskItem.createDiv({ cls: 'task-info' });
|
||||
taskInfo.createDiv({
|
||||
cls: `task-item-title task-priority-${task.priority}`,
|
||||
text: task.title
|
||||
});
|
||||
|
||||
if (task.due) {
|
||||
taskInfo.createDiv({
|
||||
cls: `task-item-due ${isTaskOverdue(task) ? 'task-overdue' : ''}`,
|
||||
text: `Due: ${task.due}`
|
||||
});
|
||||
}
|
||||
|
||||
const taskMeta = taskItem.createDiv({ cls: 'task-item-metadata' });
|
||||
|
||||
taskMeta.createDiv({
|
||||
cls: `task-status task-status-${task.status.replace(/\s+/g, '-').toLowerCase()}`,
|
||||
text: task.status
|
||||
});
|
||||
|
||||
// Add click handler to open task
|
||||
taskItem.addEventListener('click', () => {
|
||||
this.openTask(task.path);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createNotesView(container: HTMLElement) {
|
||||
// Get the selected date as a string for display
|
||||
const selectedDate = this.getSingleSelectedDate();
|
||||
const dateText = selectedDate ? `Notes for ${format(selectedDate, 'MMM d, yyyy')}` : 'All Notes';
|
||||
|
||||
container.createEl('h3', { text: dateText });
|
||||
|
||||
// Notes list
|
||||
const notesList = container.createDiv({ cls: 'notes-list' });
|
||||
|
||||
// Get notes for the current view
|
||||
const notes = await this.getNotesForView();
|
||||
|
||||
if (notes.length === 0) {
|
||||
// Placeholder for empty notes list
|
||||
notesList.createEl('p', { text: 'No notes found for the selected date.' });
|
||||
} else {
|
||||
// Create note items
|
||||
notes.forEach(note => {
|
||||
const noteItem = notesList.createDiv({ cls: 'note-item' });
|
||||
|
||||
noteItem.createDiv({ cls: 'note-item-title', text: note.title });
|
||||
|
||||
// Add created date if available
|
||||
if (note.createdDate) {
|
||||
const dateStr = note.createdDate.indexOf('T') > 0
|
||||
? format(new Date(note.createdDate), 'MMM d, yyyy h:mm a')
|
||||
: note.createdDate;
|
||||
noteItem.createDiv({ cls: 'note-item-date', text: `Created: ${dateStr}` });
|
||||
}
|
||||
|
||||
if (note.tags && note.tags.length > 0) {
|
||||
const tagContainer = noteItem.createDiv({ cls: 'note-item-tags' });
|
||||
note.tags.forEach(tag => {
|
||||
tagContainer.createSpan({ cls: 'note-tag', text: tag });
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handler to open note
|
||||
noteItem.addEventListener('click', () => {
|
||||
this.openNote(note.path);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createTimeblockView(container: HTMLElement) {
|
||||
container.createEl('h3', { text: 'Timeblock' });
|
||||
|
||||
// Get the daily note for the specific date
|
||||
const date = this.getSingleSelectedDate();
|
||||
|
||||
if (!date) {
|
||||
// No specific date selected in current view
|
||||
container.createEl('p', { text: 'Select a specific date to view timeblocks.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const dailyNotePath = normalizePath(`${this.plugin.settings.dailyNotesFolder}/${dateStr}.md`);
|
||||
|
||||
// Check if the daily note exists
|
||||
const fileExists = await this.app.vault.adapter.exists(dailyNotePath);
|
||||
if (!fileExists) {
|
||||
container.createEl('p', { text: `No daily note exists for ${format(date, 'MMMM d, yyyy')}. Click a date to create one.` });
|
||||
|
||||
const createButton = container.createEl('button', { text: 'Create daily note', cls: 'create-note-button' });
|
||||
createButton.addEventListener('click', () => {
|
||||
this.navigateToDailyNote(date);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeblock editor
|
||||
const timeblockEditor = container.createDiv({ cls: 'timeblock-editor' });
|
||||
|
||||
// Get the timeblock content from the daily note
|
||||
const file = this.app.vault.getAbstractFileByPath(dailyNotePath);
|
||||
if (file instanceof TFile) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const timeblockContent = extractTimeblockContent(content);
|
||||
|
||||
if (timeblockContent) {
|
||||
// Create a timeblock table
|
||||
const table = timeblockEditor.createEl('table', { cls: 'timeblock-table' });
|
||||
|
||||
// Parse the timeblock content and create rows
|
||||
const rows = timeblockContent.trim().split('\n').slice(1); // Skip header row
|
||||
|
||||
rows.forEach(row => {
|
||||
const [time, activity] = row.split('|').map(cell => cell.trim()).filter(cell => cell);
|
||||
|
||||
if (time && time !== '----') {
|
||||
const tr = table.createEl('tr');
|
||||
tr.createEl('td', { cls: 'timeblock-time', text: time });
|
||||
|
||||
const activityTd = tr.createEl('td', { cls: 'timeblock-activity' });
|
||||
if (activity) {
|
||||
activityTd.textContent = activity;
|
||||
}
|
||||
|
||||
// Make the activity cell editable
|
||||
activityTd.addEventListener('click', () => {
|
||||
this.editTimeblockActivity(date, time, activityTd, dailyNotePath);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No timeblock found
|
||||
timeblockEditor.createEl('p', { text: 'No timeblock found in the daily note.' });
|
||||
|
||||
const generateButton = timeblockEditor.createEl('button', {
|
||||
text: 'Generate timeblock',
|
||||
cls: 'generate-timeblock-button'
|
||||
});
|
||||
|
||||
generateButton.addEventListener('click', async () => {
|
||||
// Generate a timeblock table and add it to the daily note
|
||||
await this.addTimeblockToNote(file);
|
||||
// Refresh the view
|
||||
this.createTimeblockView(container);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getTasksForView(): Promise<TaskInfo[]> {
|
||||
const tasksFolder = this.plugin.settings.tasksFolder;
|
||||
const result: TaskInfo[] = [];
|
||||
|
||||
// Ensure tasks folder exists
|
||||
const folderExists = await this.app.vault.adapter.exists(tasksFolder);
|
||||
if (!folderExists) return result;
|
||||
|
||||
// Get all files in the tasks folder
|
||||
const files = this.app.vault.getFiles().filter(file =>
|
||||
file.path.startsWith(tasksFolder) && file.extension === 'md'
|
||||
);
|
||||
|
||||
// Extract task information from each file
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await this.app.vault.read(file);
|
||||
const taskInfo = extractTaskInfo(content, file.path);
|
||||
|
||||
if (taskInfo) {
|
||||
// Filter based on current view date if it's a specific date
|
||||
if (this.viewType === 'month' || this.viewType === 'week') {
|
||||
// If we're in a specific date view, filter only tasks due in the current month/week
|
||||
if (taskInfo.due) {
|
||||
const dueDate = new Date(taskInfo.due);
|
||||
const startDate = this.getViewStartDate();
|
||||
const endDate = this.getViewEndDate();
|
||||
|
||||
// Only include tasks due in the current view range
|
||||
if (dueDate >= startDate && dueDate <= endDate) {
|
||||
result.push(taskInfo);
|
||||
}
|
||||
} else {
|
||||
// Include tasks without due dates
|
||||
result.push(taskInfo);
|
||||
}
|
||||
} else {
|
||||
// For all other views, include all tasks
|
||||
result.push(taskInfo);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error processing task file ${file.path}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort tasks by due date, then priority
|
||||
return result.sort((a, b) => {
|
||||
// Sort by due date
|
||||
if (a.due && b.due) {
|
||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||
}
|
||||
// Tasks with due dates come before tasks without
|
||||
if (a.due && !b.due) return -1;
|
||||
if (!a.due && b.due) return 1;
|
||||
|
||||
// Then sort by priority
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
return priorityOrder[a.priority as keyof typeof priorityOrder] - priorityOrder[b.priority as keyof typeof priorityOrder];
|
||||
});
|
||||
}
|
||||
|
||||
async getNotesForView(): Promise<NoteInfo[]> {
|
||||
const notesFolder = this.plugin.settings.notesFolder;
|
||||
const result: NoteInfo[] = [];
|
||||
|
||||
// Ensure notes folder exists
|
||||
const folderExists = await this.app.vault.adapter.exists(notesFolder);
|
||||
if (!folderExists) return result;
|
||||
|
||||
// Get all markdown files in the notes folder
|
||||
const files = this.app.vault.getFiles().filter(file =>
|
||||
file.path.startsWith(notesFolder) && file.extension === 'md'
|
||||
);
|
||||
|
||||
// Get the selected date
|
||||
const selectedDate = this.getSingleSelectedDate();
|
||||
const selectedDateStr = selectedDate ? format(selectedDate, 'yyyy-MM-dd') : null;
|
||||
|
||||
// Extract note information from each file
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await this.app.vault.read(file);
|
||||
const noteInfo = extractNoteInfo(content, file.path, file);
|
||||
|
||||
if (noteInfo) {
|
||||
// Only include notes that match the selected date's creation date
|
||||
if (!selectedDateStr || (noteInfo.createdDate && noteInfo.createdDate.startsWith(selectedDateStr))) {
|
||||
result.push(noteInfo);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error processing note file ${file.path}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort notes by title
|
||||
return result.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
async addTimeblockToNote(file: TFile) {
|
||||
try {
|
||||
// Read the content
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
// Check if it already has a timeblock section
|
||||
if (extractTimeblockContent(content)) {
|
||||
new Notice('Note already has a timeblock section');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the settings for timeblock generation
|
||||
const startTime = parseTime(this.plugin.settings.timeblockStartTime);
|
||||
const endTime = parseTime(this.plugin.settings.timeblockEndTime);
|
||||
const interval = parseInt(this.plugin.settings.timeblockInterval);
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
new Notice('Invalid timeblock settings');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the timeblock table
|
||||
const timeblockTable = this.plugin.generateTimeblockTable();
|
||||
|
||||
// Append the timeblock to the content
|
||||
const updatedContent = `${content.trim()}\n\n## Timeblock\n\n${timeblockTable}\n`;
|
||||
|
||||
// Update the file
|
||||
await this.app.vault.modify(file, updatedContent);
|
||||
|
||||
new Notice('Timeblock added to daily note');
|
||||
} catch (e) {
|
||||
console.error('Error adding timeblock to note:', e);
|
||||
new Notice('Error adding timeblock to note');
|
||||
}
|
||||
}
|
||||
|
||||
async editTimeblockActivity(date: Date, time: string, cell: HTMLElement, notePath: string) {
|
||||
// Create an input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = cell.textContent || '';
|
||||
input.className = 'timeblock-edit-input';
|
||||
input.style.width = '100%';
|
||||
|
||||
// Replace the cell content with the input
|
||||
cell.empty();
|
||||
cell.appendChild(input);
|
||||
input.focus();
|
||||
|
||||
// Handle saving on enter or blur
|
||||
const saveChange = async () => {
|
||||
const newValue = input.value;
|
||||
cell.textContent = newValue;
|
||||
|
||||
// Update the note file
|
||||
await this.updateTimeblockInNote(time, newValue, notePath);
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
await saveChange();
|
||||
} else if (e.key === 'Escape') {
|
||||
// Restore original content
|
||||
cell.textContent = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', async () => {
|
||||
await saveChange();
|
||||
});
|
||||
}
|
||||
|
||||
async updateTimeblockInNote(time: string, activity: string, notePath: string) {
|
||||
try {
|
||||
const file = this.app.vault.getAbstractFileByPath(notePath);
|
||||
if (!(file instanceof TFile)) return;
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
const timeblockMatch = content.match(/## Timeblock\s*\n([^#]*)/);
|
||||
|
||||
if (!timeblockMatch) return;
|
||||
|
||||
const timeblockContent = timeblockMatch[1].trim();
|
||||
const lines = timeblockContent.split('\n');
|
||||
|
||||
// Find the line with the matching time
|
||||
let updatedTimeblock = lines.map(line => {
|
||||
if (line.includes(`| ${time} |`)) {
|
||||
return `| ${time} | ${activity} |`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
// Replace the timeblock in the content
|
||||
const updatedContent = content.replace(
|
||||
/## Timeblock\s*\n([^#]*)/,
|
||||
`## Timeblock\n\n${updatedTimeblock}\n`
|
||||
);
|
||||
|
||||
// Update the file
|
||||
await this.app.vault.modify(file, updatedContent);
|
||||
} catch (e) {
|
||||
console.error('Error updating timeblock in note:', e);
|
||||
new Notice('Error updating timeblock');
|
||||
}
|
||||
}
|
||||
|
||||
getSingleSelectedDate(): Date | null {
|
||||
if (this.viewType === 'week') {
|
||||
// For week view, return the current date
|
||||
return this.currentDate;
|
||||
} else {
|
||||
// For month view, if the user has selected a specific day, return that
|
||||
return this.currentDate;
|
||||
}
|
||||
}
|
||||
|
||||
getViewStartDate(): Date {
|
||||
if (this.viewType === 'month') {
|
||||
// First day of the month
|
||||
return new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);
|
||||
} else if (this.viewType === 'week') {
|
||||
// Start of the week (Sunday)
|
||||
const date = new Date(this.currentDate);
|
||||
const dayOfWeek = date.getDay();
|
||||
date.setDate(date.getDate() - dayOfWeek);
|
||||
return date;
|
||||
}
|
||||
return this.currentDate;
|
||||
}
|
||||
|
||||
getViewEndDate(): Date {
|
||||
if (this.viewType === 'month') {
|
||||
// Last day of the month
|
||||
return new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 0);
|
||||
} else if (this.viewType === 'week') {
|
||||
// End of the week (Saturday)
|
||||
const date = new Date(this.currentDate);
|
||||
const dayOfWeek = date.getDay();
|
||||
date.setDate(date.getDate() + (6 - dayOfWeek));
|
||||
return date;
|
||||
}
|
||||
return this.currentDate;
|
||||
}
|
||||
|
||||
openTask(path: string) {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
this.app.workspace.getLeaf(false).openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
openNote(path: string) {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
this.app.workspace.getLeaf(false).openFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToDailyNote(date: Date) {
|
||||
await this.plugin.navigateToDailyNote(date);
|
||||
}
|
||||
}
|
||||
43
src/views/NotesView.ts
Normal file
43
src/views/NotesView.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { View, WorkspaceLeaf } from 'obsidian';
|
||||
import ChronoSyncPlugin from '../main';
|
||||
import { NOTES_VIEW_TYPE } from '../types';
|
||||
|
||||
export class NotesView extends View {
|
||||
plugin: ChronoSyncPlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ChronoSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return NOTES_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Notes';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'file-text';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
// Clear and prepare the content element
|
||||
const contentEl = this.containerEl;
|
||||
contentEl.empty();
|
||||
|
||||
// Add a container for our view content
|
||||
const container = contentEl.createDiv({ cls: 'chronosync-container' });
|
||||
|
||||
// Create and add UI elements
|
||||
container.createEl('h2', { text: 'Notes' });
|
||||
|
||||
// This will be implemented in more detail later
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
// Clean up when the view is closed
|
||||
this.containerEl.empty();
|
||||
}
|
||||
}
|
||||
43
src/views/TaskListView.ts
Normal file
43
src/views/TaskListView.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { View, WorkspaceLeaf } from 'obsidian';
|
||||
import ChronoSyncPlugin from '../main';
|
||||
import { TASK_LIST_VIEW_TYPE } from '../types';
|
||||
|
||||
export class TaskListView extends View {
|
||||
plugin: ChronoSyncPlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ChronoSyncPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return TASK_LIST_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Tasks';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'check-square';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
// Clear and prepare the content element
|
||||
const contentEl = this.containerEl;
|
||||
contentEl.empty();
|
||||
|
||||
// Add a container for our view content
|
||||
const container = contentEl.createDiv({ cls: 'chronosync-container' });
|
||||
|
||||
// Create and add UI elements
|
||||
container.createEl('h2', { text: 'Tasks' });
|
||||
|
||||
// This will be implemented in more detail later
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
// Clean up when the view is closed
|
||||
this.containerEl.empty();
|
||||
}
|
||||
}
|
||||
520
styles.css
Normal file
520
styles.css
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
/* ChronoSync Plugin Styles */
|
||||
|
||||
/* Main container for all views */
|
||||
.chronosync-container {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Calendar Month View */
|
||||
.calendar-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.current-month {
|
||||
font-weight: bold;
|
||||
min-width: 8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.view-switcher button {
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.view-switcher button.active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.calendar-grid-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.calendar-day-header {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
position: relative;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.calendar-day:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.calendar-day.selected,
|
||||
.week-day-header.selected {
|
||||
border: 2px solid var(--interactive-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Style for dates that are selected but not today */
|
||||
.calendar-day.selected:not(.today),
|
||||
.week-day-header.selected:not(.today) {
|
||||
background-color: var(--background-modifier-form-field);
|
||||
}
|
||||
|
||||
/* Style for dates that are both today and selected */
|
||||
.calendar-day.today.selected,
|
||||
.week-day-header.today.selected {
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent);
|
||||
}
|
||||
|
||||
.calendar-day.outside-month {
|
||||
color: var(--text-faint);
|
||||
background-color: var(--background-primary-alt);
|
||||
}
|
||||
|
||||
.day-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.indicator-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.has-note {
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.has-task {
|
||||
background-color: var(--text-error);
|
||||
}
|
||||
|
||||
.is-important {
|
||||
background-color: var(--text-warning);
|
||||
}
|
||||
|
||||
/* Main Content Area with Tabs */
|
||||
.chronosync-main-content {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chronosync-tabs {
|
||||
display: flex;
|
||||
background-color: var(--background-secondary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.chronosync-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.chronosync-tab:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.chronosync-tab.active {
|
||||
background-color: var(--background-primary);
|
||||
border-bottom: 2px solid var(--interactive-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chronosync-content-area {
|
||||
padding: 1rem;
|
||||
background-color: var(--background-primary);
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Task Filters */
|
||||
.task-filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-task-button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Task List */
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.task-item-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-item-metadata {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.task-priority-high {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.task-priority-normal {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.task-priority-low {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.task-status-open {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.task-status-in-progress {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.task-status-done {
|
||||
color: var(--text-success);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-overdue {
|
||||
color: var(--text-error);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Task Creation Modal */
|
||||
.task-creation-modal {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* Notes List */
|
||||
.notes-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.note-item-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.note-item-date {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.note-item-tags {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.note-tag {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Timeblock Editor */
|
||||
.timeblock-edit-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.25rem;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-bottom: 1px dashed var(--interactive-accent);
|
||||
}
|
||||
.timeblock-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeblock-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.timeblock-table td,
|
||||
.timeblock-table th {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.timeblock-time {
|
||||
font-weight: bold;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.timeblock-activity {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.timeblock-activity:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.generate-timeblock-button, .create-note-button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Week View */
|
||||
.week-view-container {
|
||||
margin-bottom: 1rem;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.week-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.week-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
background-color: var(--background-secondary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.week-day-header {
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.week-day-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.week-day-header.today {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.week-day-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.week-day-date {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.week-time-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.week-time-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.week-time-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.week-time-label {
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: 60px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.week-time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.week-time-cell {
|
||||
padding: 0.25rem;
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
min-height: 2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.week-time-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.week-time-cell:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.week-time-cell.current-time {
|
||||
background-color: rgba(var(--interactive-accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.calendar-grid {
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
min-height: 1.5rem;
|
||||
padding: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.task-creation-modal {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue