├── .gitattributes
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── ding.mp3
├── icon.png
├── package-lock.json
├── package.json
├── screenshots
├── logseq_time_tracker_main_demo.gif
├── logseq_time_tracker_settings.png
├── logseq_time_tracker_switch_timer_mode_demo.gif
├── logseq_time_tracker_totalTimeTracked_demo.gif
└── plugin_vs_native_timetracking.png
└── src
├── index.html
└── index.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build plugin
2 |
3 | on:
4 | push:
5 | # Sequence of patterns matched against refs/tags
6 | tags:
7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
8 |
9 | env:
10 | PLUGIN_NAME: logseq-time-tracker-plugin
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: '16.x' # You might need to adjust this value to your own version
22 | - name: Build
23 | id: build
24 | run: |
25 | npm i && npm run build
26 | mkdir ${{ env.PLUGIN_NAME }}
27 | cp README.md package.json icon.png ${{ env.PLUGIN_NAME }}
28 | mv dist ${{ env.PLUGIN_NAME }}
29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
30 | ls
31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
32 | - name: Create Release
33 | uses: ncipollo/release-action@v1
34 | id: create_release
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | VERSION: ${{ github.ref }}
38 | with:
39 | allowUpdates: true
40 | draft: false
41 | prerelease: false
42 |
43 | - name: Upload zip file
44 | id: upload_zip
45 | uses: actions/upload-release-asset@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | with:
49 | upload_url: ${{ steps.create_release.outputs.upload_url }}
50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
52 | asset_content_type: application/zip
53 |
54 | - name: Upload package.json
55 | id: upload_metadata
56 | uses: actions/upload-release-asset@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | upload_url: ${{ steps.create_release.outputs.upload_url }}
61 | asset_path: ./package.json
62 | asset_name: package.json
63 | asset_content_type: application/json
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Microbundle cache
59 | .rpt2_cache/
60 | .rts2_cache_cjs/
61 | .rts2_cache_es/
62 | .rts2_cache_umd/
63 |
64 | # Optional REPL history
65 | .node_repl_history
66 |
67 | # Output of 'npm pack'
68 | *.tgz
69 |
70 | # Yarn Integrity file
71 | .yarn-integrity
72 |
73 | # dotenv environment variables file
74 | .env
75 | .env.test
76 | .env.production
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 | .parcel-cache
81 |
82 | # Next.js build output
83 | .next
84 | out
85 |
86 | # Nuxt.js build / generate output
87 | .nuxt
88 | dist
89 |
90 | # Gatsby files
91 | .cache/
92 | # Comment in the public line in if your project uses Gatsby and not Next.js
93 | # https://nextjs.org/blog/next-9-1#public-directory-support
94 | # public
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # TernJS port file
109 | .tern-port
110 |
111 | # Stores VSCode versions used for testing VSCode extensions
112 | .vscode-test
113 | .vscode
114 |
115 | # yarn v2
116 | .yarn/cache
117 | .yarn/unplugged
118 | .yarn/build-state.yml
119 | .yarn/install-state.gz
120 | .pnp.*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 vyleung
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## logseq-time-tracker-plugin
2 | - [Demo videos](https://loom.com/share/folder/9644cc808d254e17826e1aeb4fba394c)
3 |
4 | ## Core Features
5 | > The **_time tracked_** and **_total time tracked_** are inserted as a block property in the following notation: `hours:minutes:seconds` (e.g. 00:17:32 → 17 minutes and 32 seconds)
6 |
7 | ### 2 Time Tracking Modes
8 | > Easily switch between the 2 time tracking modes by clicking on the *letter next to the stopwatch icon in the toolbar
9 | 
10 | - #### Stopwatch (*S)
11 | - This mode counts up time
12 | - #### Pomodoro Timer (*P)
13 | - This mode counts down time [(additional info)](https://en.wikipedia.org/wiki/Pomodoro_Technique)
14 | - The length of the pomodoro and break interval can be configured in the [settings](#settings)
15 |
16 | ### Keyboard shortcuts to start/stop the timer and get/update the total time tracked
17 | - Usage: click on the task (as if to edit it) → activate the keyboard shortcut (can be configured in the [settings](#settings))
18 |
19 | ### Start and stop tracking time for a _new_ or _existing_ task in 3 ways:
20 | - Block context menu (right-click on bullet)
21 | - Plugin UI
22 | - New tasks created via the plugin UI are added to the daily journal page by default (but can be added to a specific page) and can be prepended with your preferred workflow (e.g. TODO/NOW) - these features can be configured in the [settings](#settings)
23 | - Slash (/) command
24 | #### Demo
25 | 
26 | > The emoji displayed next to the task being time tracked can be configured in the [settings](#settings)
27 |
28 | ### Get and update the total time tracked for a parent task with child(ren) tasks in 2 ways:
29 | - Block context menu (right-click on bullet)
30 | - Slash (/) command
31 | - After getting the total time tracked for the first time, an inline refresh button will appear to make it easier to update the total time tracked (the color and position of the button can be configured in the [settings](#settings))
32 | - 🚨 **NOTE:** Always use the block context menu or slash command to get the inital total time tracked. Please do **NOT** copy and paste `{{renderer :refreshTotalTimeTracked}}` to other blocks
33 |
34 | #### Demo
35 | 
36 |
37 | ## Additional Features
38 | > These additional features are **_disabled by default_** and can be configured in the [settings](#settings)
39 |
40 | ### Show log entries that mirror Logseq's native time tracking functionality
41 |  _Task state: TODO (red checkbox)_
42 |
43 | ### Get notified after a customizable interval of time
44 | - Similar to a pomodoro timer - a **"ding"** sound will play and/or a **system notification** will appear at the end of _**each**_ interval (e.g. every 25 minutes), but the stopwatch will continue to track time until you stop the timer
45 |
46 | ## Installation
47 | ### Preparation
48 | 1. Click the 3 dots in the righthand corner → `Settings` → `Advanced` → Enable `Developer mode` and `Plug-in system`
49 | 2. Click the 3 dots in the righthand corner → `Plugins` – OR – Use keyboard shortcut `Esc t p`
50 |
51 | ### Load plugin via the marketplace (not available yet)
52 |
53 | ### Load plugin manually
54 | 1. Download the [latest release](https://github.com/vyleung/logseq-time-tracker-plugin/releases) of the plugin (e.g logseq-time-tracker-plugin-v.1.0.0.zip) from Github
55 | 2. Unzip the file
56 | 3. Navigate to plugins (Click the 3 dots → `Plugins` – OR – Use keyboard shortcut `Esc t p`) → `Load unpacked plugin` → Select the folder of the unzipped file
57 |
58 | #### Settings
59 | - Each time you make changes to the plugin settings, please reload the plugin to ensure that all settings are updated
60 | 
61 |
62 | ## License
63 | MIT
64 |
65 | ## Credits
66 | - Plugin Marketplace Icon: Stopwatch icons created by Freepik - Flaticon
67 | - Icons used in the plugin: [Tabler Icons](https://tablericons.com/) and [Feather Icons](https://feathericons.com/)
68 |
69 | ## Support
70 | If you find this plugin useful, consider buying me a coffee 🙂
71 |
--------------------------------------------------------------------------------
/ding.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/ding.mp3
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/icon.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq-time-tracker-plugin",
3 | "version": "2.1.0",
4 | "description": "Track time spent on tasks",
5 | "main": "dist/index.html",
6 | "targets": {
7 | "main": false
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "build": "parcel build --no-source-maps src/index.html --public-url ./"
12 | },
13 | "keywords": [],
14 | "author": "vyleung",
15 | "license": "MIT",
16 | "dependencies": {
17 | "@logseq/libs": "^0.0.1-alpha.35",
18 | "date-fns": "^2.28.0",
19 | "driftless": "^2.0.3",
20 | "text-field-edit": "^3.1.9001"
21 | },
22 | "logseq": {
23 | "id": "logseq-time-tracker-plugin",
24 | "title": "logseq-time-tracker-plugin",
25 | "icon": "./icon.png"
26 | },
27 | "devDependencies": {
28 | "parcel": "^2.4.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/screenshots/logseq_time_tracker_main_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/screenshots/logseq_time_tracker_main_demo.gif
--------------------------------------------------------------------------------
/screenshots/logseq_time_tracker_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/screenshots/logseq_time_tracker_settings.png
--------------------------------------------------------------------------------
/screenshots/logseq_time_tracker_switch_timer_mode_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/screenshots/logseq_time_tracker_switch_timer_mode_demo.gif
--------------------------------------------------------------------------------
/screenshots/logseq_time_tracker_totalTimeTracked_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/screenshots/logseq_time_tracker_totalTimeTracked_demo.gif
--------------------------------------------------------------------------------
/screenshots/plugin_vs_native_timetracking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vyleung/logseq-time-tracker-plugin/6bd799fdd0c1a0281e2c40316fe7191d713b344b/screenshots/plugin_vs_native_timetracking.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
111 |
112 |
113 |
114 |
115 |
119 |
120 |
126 |
127 |
133 |
134 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import "@logseq/libs";
2 | import {setDriftlessTimeout, setDriftlessInterval, clearDriftless} from "driftless";
3 | import {format, getUnixTime} from "date-fns";
4 | import * as textFieldEdit from 'text-field-edit';
5 | import { __esModule } from "driftless";
6 |
7 | let h;
8 | let m;
9 | let s;
10 | let day;
11 | let hr;
12 | let min;
13 | let sec;
14 | let hours;
15 | let minutes;
16 | let seconds;
17 |
18 | let stopwatch_hr_duration;
19 | let stopwatch_min_duration;
20 | let stopwatch_sec_duration;
21 | let pomo_hr_duration;
22 | let pomo_min_duration;
23 | let pomo_sec_duration;
24 | let pomo_hr_tracked;
25 | let pomo_min_tracked;
26 | let pomo_sec_tracked;
27 | let total_time_tracked_sec;
28 |
29 | let first_start = true;
30 | let pause = true;
31 | let pomo_break = false;
32 | let pomo_end = false;
33 | let timer;
34 | let task_name;
35 | let duration;
36 | let duration_tracked;
37 | let pomo_notif_active;
38 | let pomo_notif;
39 | let system_notif_active;
40 | let system_notif;
41 | let pomo_duration;
42 | let pomo_interval;
43 | let time_elapsed_pomo;
44 |
45 | let todays_journal_title;
46 | let workflow_format;
47 | let selected_task_uuid;
48 | let block_uuid;
49 | let last_block_uuid;
50 | let created_block_uuid;
51 | let block_content;
52 |
53 | let start_time;
54 | let current_time;
55 | let time_elapsed_stopwatch;
56 | let log_entry_start;
57 | let log_entry_stop;
58 |
59 | const app = document.getElementById("app");
60 | const add_task = document.getElementById("add-task");
61 | const start_button = document.getElementById("start-button");
62 | const stop_button = document.getElementById("stop-button");
63 | const settings_button = document.getElementById("settings-button");
64 | const notif_sound = document.getElementById("pomo-notif");
65 | const refresh_renderer = "{{renderer :refreshTotalTimeTracked}}";
66 | let settings = [
67 | {
68 | key: "!",
69 | title: "NOTE",
70 | description: "Please refresh Logseq/reload the plugin after changing any settings to ensure that they're saved properly",
71 | type: "object"
72 | },
73 | {
74 | key: "KeyboardShortcut_Timer",
75 | title: "Keyboard shortcut to start/stop the timer",
76 | description: "This is the keyboard shortcut used to start/stop the timer (default: ctrl+s)",
77 | type: "string",
78 | default: "ctrl+s"
79 | },
80 | {
81 | key: "KeyboardShortcut_getTotalTimeTracked",
82 | title: "Keyboard shortcut to get/update the total time tracked",
83 | description: "This is the keyboard shortcut used to get/update the total time tracked (default: ctrl+g)",
84 | type: "string",
85 | default: "ctrl+g"
86 | },
87 | {
88 | key: "DefaultMode",
89 | title: "Default mode of time tracker?",
90 | description: "Would you like the default mode of the time tracker to be a stopwatch (counts up) or a pomodoro timer (counts down)?",
91 | default: "stopwatch",
92 | enumPicker: "radio",
93 | enumChoices: ["stopwatch", "pomodoro timer"],
94 | type: "enum"
95 | },
96 | {
97 | key: "Logs",
98 | title: "Show log entries?",
99 | description: "Check the box if you would like each log entry stored under the :LOG-ENTRIES: drawer",
100 | default: false,
101 | type: "boolean",
102 | enumPicker: "checkbox"
103 | },
104 | {
105 | key: "Workflow",
106 | title: "Prepend workflow to task?",
107 | description: "Check the box if you would like to prepend new tasks with your preferred workflow (e.g. TODO/NOW)",
108 | default: false,
109 | type: "boolean",
110 | enumPicker: "checkbox"
111 | },
112 | {
113 | key: "AddtoPage",
114 | title: "Add task to mentioned page?",
115 | description: "Check the box if you would like to add the new task to the page mentioned in the task. If unchecked, the task will be added to the daily journal page",
116 | default: false,
117 | type: "boolean",
118 | enumPicker: "checkbox"
119 | },
120 | {
121 | key: "PomoNotification_System",
122 | title: "Get a system notification every X minutes?",
123 | description: 'Check the box if you would like to receive a system notification after every X minutes',
124 | default: false,
125 | type: "boolean",
126 | enumPicker: "checkbox"
127 | },
128 | {
129 | key: "PomoNotification_Plugin",
130 | title: "Get notified every X minutes?",
131 | description: 'Check the box if you would like to hear a "ding" after every X minutes',
132 | default: false,
133 | type: "boolean",
134 | enumPicker: "checkbox"
135 | },
136 | {
137 | key: "PomoDuration",
138 | title: "Pomo interval duration?",
139 | description: "Enter a duration in minutes (e.g. 90 for 1.5 hours)",
140 | default: 25,
141 | type: "number"
142 | },
143 | {
144 | key: "PomoBreakDuration",
145 | title: "Pomo break duration?",
146 | description: "Enter a duration in minutes (e.g. 90 for 1.5 hours)",
147 | default: 5,
148 | type: "number"
149 | },
150 | {
151 | key: "NotifVolume",
152 | title: "Volume level of notification?",
153 | description: 'How loud would you like the "ding" sound to be?',
154 | default: "normal",
155 | enumPicker: "radio",
156 | enumChoices: ["quiet", "normal", "loud"],
157 | type: "enum"
158 | },
159 | {
160 | key: "AfterPomoEnd",
161 | title: "What happens after the pomodoro interval ends?",
162 | description: "Start a break and after the break ends, start another pomodoro interval – OR – Stop the pomodoro timer",
163 | default: "break, then start another timer",
164 | enumPicker: "radio",
165 | enumChoices: ["break, then start another timer", "stop timer"],
166 | type: "enum"
167 | },
168 | {
169 | key: "RefreshButtonColor",
170 | title: "Color of the refresh button?",
171 | description: "This is the color of the refresh button (default: #F7BF18)",
172 | type: "string",
173 | default: "#F7BF18"
174 | },
175 | {
176 | key: "RefreshButtonMarginTop",
177 | title: "Margin-top for the refresh button?",
178 | description: "This is used to align the refresh button with text content in the blocks (default: -2.75em) - To move the button up, make the number more negative (e.g. -3.5em). To move the button down, make the number more positive (e.g. -1.5em)",
179 | type: "string",
180 | default: "-2.75em"
181 | },
182 | {
183 | key: "ActiveTaskEmoji",
184 | title: "Emoji to indicate active task?",
185 | description: "Add an emoji as a visual indicator of the task that's being time tracked (default: ⏱). Leave this setting blank if you don't want an emoji to be shown",
186 | type: "string",
187 | default: "⏱"
188 | }
189 | ]
190 | logseq.useSettingsSchema(settings);
191 |
192 | function addTimeTracked() {
193 | // existing time tracked = h + m + s
194 | if (pomo_end == true) {
195 | total_time_tracked_sec = (time_elapsed_pomo == 0) ? ((h + m + s) + time_elapsed_stopwatch) : ((h + m + s) + time_elapsed_pomo - 1);
196 | }
197 | else {
198 | total_time_tracked_sec = (time_elapsed_pomo == 0) ? ((h + m + s) + time_elapsed_stopwatch) : ((h + m + s) + time_elapsed_pomo);
199 | }
200 |
201 | hours = Math.floor(total_time_tracked_sec/3600);
202 | minutes = Math.floor((total_time_tracked_sec/60) % 60);
203 | seconds = Math.floor(total_time_tracked_sec % 60);
204 |
205 | // hours
206 | hours = (hours.toString().length == 1) ? `0${hours}` : `${hours}`;
207 |
208 | // minutes
209 | minutes = (minutes.toString().length == 1) ? `0${minutes}` : `${minutes}`;
210 |
211 | // seconds
212 | seconds = (seconds.toString().length == 1) ? `0${seconds}` : `${seconds}`;
213 | }
214 |
215 | function getTotalTimeTracked(e) {
216 | let parent_time_entry = 0;
217 | let children_time_entry = 0;
218 | let time_tracked_total = 0;
219 | let parent_block_content;
220 |
221 | if (e.uuid == undefined) {
222 | // gets the block uuid that the refresh button is located in
223 | if (e.dataset.refreshUuid == undefined) {
224 | logseq.App.showMsg("No parent task selected", "warning");
225 | }
226 | else {
227 | e.uuid = e.dataset.refreshUuid;
228 | }
229 | }
230 |
231 | logseq.Editor.getBlockProperties(e.uuid).then(block_property => {
232 | // if the parent block does NOT have any block properties
233 | if (block_property == null) {
234 | parent_time_entry = 0;
235 | }
236 | // if the parent block does have block properties
237 | else {
238 | // if the parent block contains both the time-tracked and time-tracked-total properties – OR – if it only has the time-tracked property, only add the time-tracked
239 | if (((block_property.timeTracked) && (block_property.timeTrackedTotal)) || ((block_property.timeTracked) && (block_property.timeTrackedTotal == undefined))) {
240 | parent_time_entry += parseInt(block_property.timeTracked.split(":")[0]*3600) + parseInt(block_property.timeTracked.split(":")[1]*60) + parseInt(block_property.timeTracked.split(":")[2]);
241 | }
242 | // if the parent block has neither the time-tracked or time-tracked-total properties – OR – if it only has the time-tracked-total property, parent_time_entry = 0 to prevent the time-tracked-total from being updated when the refresh button is clicked
243 | else {
244 | parent_time_entry = 0;
245 | }
246 | }
247 | });
248 |
249 | // ref for traversing through a block and their children: https://gist.github.com/umidjons/6865350#file-walk-dom-js
250 | logseq.Editor.getBlock(e.uuid, {includeChildren: true}).then(parent_block => {
251 | function loop(block) {
252 | logseq.Editor.getBlock(block.uuid, {includeChildren: true}).then(tree_block => {
253 | // if the block has children
254 | if (tree_block.children.length > 0) {
255 | tree_block.children.forEach(child_block => {
256 | logseq.Editor.getBlockProperties(child_block.uuid).then(block_properties => {
257 | // if the child block contains both the time-tracked and time-tracked-total properties – OR – if it only has the time-tracked-total property, add the time-tracked-total
258 | if (((block_properties.timeTracked) && (block_properties.timeTrackedTotal)) || ((block_properties.timeTracked == undefined) && (block_properties.timeTrackedTotal))) {
259 | children_time_entry += parseInt(block_properties.timeTrackedTotal.split(":")[0]*3600) + parseInt(block_properties.timeTrackedTotal.split(":")[1]*60) + parseInt(block_properties.timeTrackedTotal.split(":")[2]);
260 | }
261 | // if the child block contains only the time-tracked property, add the time-tracked
262 | else if ((block_properties.timeTracked) && (block_properties.timeTrackedTotal == undefined)) {
263 | children_time_entry += parseInt(block_properties.timeTracked.split(":")[0]*3600) + parseInt(block_properties.timeTracked.split(":")[1]*60) + parseInt(block_properties.timeTracked.split(":")[2]);
264 | }
265 | });
266 | loop(child_block);
267 | });
268 | }
269 | // add the time tracked of the parent block and its child(ren) blocks to get total tracked time
270 | time_tracked_total = parent_time_entry + children_time_entry;
271 | });
272 | }
273 | parent_block_content = parent_block.content;
274 | loop(parent_block);
275 |
276 | setDriftlessTimeout(() => {
277 | // convert total time tracked to 00:00:00
278 | if (time_tracked_total <= 86399) {
279 | hr = Math.floor(time_tracked_total/3600);
280 | min = Math.floor((time_tracked_total/60) % 60);
281 | sec = Math.floor(time_tracked_total % 60);
282 | }
283 | else {
284 | // convert total time tracked to 00:00:00:00
285 | day = Math.floor(time_tracked_total/86400);
286 | hr = Math.floor(((time_tracked_total/3600) % 60) % 24);
287 | min = Math.floor((time_tracked_total/60) % 60);
288 | sec = Math.floor(time_tracked_total % 60);
289 | }
290 |
291 | if (time_tracked_total == 0) {
292 | // logseq.App.showMsg("No time tracked", "warning");
293 | console.log('0');
294 | }
295 | // 00:00:seconds
296 | else if ((time_tracked_total >= 1) && (time_tracked_total <= 59)) {
297 | sec = (time_tracked_total.toString().length == 1) ? `0${time_tracked_total}`: `${time_tracked_total}`;
298 |
299 | logseq.Editor.getBlockProperty(e.uuid, "time-tracked-total").then(a => {
300 | // if the block does NOT have the time-tracked-total property
301 | if (a == null) {
302 | logseq.Editor.updateBlock(e.uuid, `${parent_block_content}\ntime-tracked-total:: 00:00:${sec} ${refresh_renderer}`);
303 | }
304 | // if the block does have the time-tracked-total property
305 | else {
306 | logseq.Editor.upsertBlockProperty(e.uuid, "time-tracked-total", `00:00:${sec} ${refresh_renderer}`);
307 | }
308 | });
309 | }
310 |
311 | // 00:minutes:seconds
312 | else if ((time_tracked_total >= 60) && (time_tracked_total <= 3599)) {
313 | // minutes
314 | min = (min.toString().length == 1) ? `0${min}` : `${min}`;
315 |
316 | // seconds
317 | sec = (sec.toString().length == 1) ? `0${sec}` : `${sec}`;
318 |
319 | logseq.Editor.getBlockProperty(e.uuid, "time-tracked-total").then(b => {
320 | // if the block does NOT have the time-tracked-total property
321 | if (b == null) {
322 | logseq.Editor.updateBlock(e.uuid, `${parent_block_content}\ntime-tracked-total:: 00:${min}:${sec} ${refresh_renderer}`);
323 | }
324 | // if the block does have the time-tracked-total property
325 | else {
326 | logseq.Editor.upsertBlockProperty(e.uuid, "time-tracked-total", `00:${min}:${sec} ${refresh_renderer}`);
327 | }
328 | });
329 | }
330 |
331 | // hours:minutes:seconds
332 | else if ((time_tracked_total >= 3600) && (time_tracked_total <= 86399)) {
333 | // hours
334 | hr = (hr.toString().length == 1) ? `0${hr}` : `${hr}`;
335 |
336 | // minutes
337 | min = (min.toString().length == 1) ? `0${min}` : `${min}`;
338 |
339 | // seconds
340 | sec = (sec.toString().length == 1) ? `0${sec}` : `${sec}`;
341 |
342 | logseq.Editor.getBlockProperty(e.uuid, "time-tracked-total").then(c => {
343 | // if the block does NOT have the time-tracked-total property
344 | if (c == null) {
345 | logseq.Editor.updateBlock(e.uuid, `${parent_block_content}\ntime-tracked-total:: ${hr}:${min}:${sec} ${refresh_renderer}`);
346 | }
347 | // if the block does have the time-tracked-total property
348 | else {
349 | logseq.Editor.upsertBlockProperty(e.uuid, "time-tracked-total", `${hr}:${min}:${sec} ${refresh_renderer}`);
350 | }
351 | });
352 | }
353 |
354 | // days:hours:minutes:seconds
355 | else if (time_tracked_total >= 84600) {
356 | // days
357 | day = (day.toString().length == 1) ? `0${day}` : `${day}`;
358 |
359 | // hours
360 | hr = (hr.toString().length == 1) ? `0${hr}` : `${hr}`;
361 |
362 | // minutes
363 | min = (min.toString().length == 1) ? `0${min}` : `${min}`;
364 |
365 | // seconds
366 | sec = (sec.toString().length == 1) ? `0${sec}` : `${sec}`;
367 |
368 | logseq.Editor.getBlockProperty(e.uuid, "time-tracked-total").then(d => {
369 | // if the block does NOT have the time-tracked-total property
370 | if (d == null) {
371 | logseq.Editor.updateBlock(e.uuid, `${parent_block_content}\ntime-tracked-total:: ${day}:${hr}:${min}:${sec} ${refresh_renderer}`);
372 | }
373 | // if the block does have the time-tracked-total property
374 | else {
375 | logseq.Editor.upsertBlockProperty(e.uuid, "time-tracked-total", `${day}:${hr}:${min}:${sec} ${refresh_renderer}`);
376 | }
377 | });
378 | }
379 | }, 500);
380 | });
381 | }
382 |
383 | function notifSoundVolume() {
384 | if (logseq.settings.NotifVolume == "quiet") {
385 | notif_sound.volume = 0.25;
386 | }
387 | else if (logseq.settings.NotifVolume == "normal") {
388 | notif_sound.volume = 0.5;
389 | }
390 | else {
391 | notif_sound.volume = 1.0;
392 | }
393 | }
394 |
395 | function startTimerBasics() {
396 | pause = false;
397 | start_time = getUnixTime(new Date());
398 | logseq.hideMainUI();
399 | app.style.display = "block";
400 | add_task.style.display = "none";
401 | stop_button.style.display = "block";
402 | start_button.style.display = "none";
403 | log_entry_start = format(new Date(), "yyyy-MM-dd EEE HH:mm:ss");
404 | time_elapsed_stopwatch = 0;
405 | time_elapsed_pomo = 0;
406 |
407 | if (first_start == true) {
408 | logseq.App.showMsg("Timer started");
409 |
410 | setDriftlessTimeout(() => {
411 | first_start = false;
412 | }, 25);
413 | }
414 |
415 | if (pomo_break == false) {
416 | pomo_interval = (pomo_duration * 60);
417 | }
418 | else {
419 | pomo_interval = (logseq.settings.PomoBreakDuration * 60);
420 | }
421 |
422 | // default mode: stopwatch
423 | if (logseq.settings.DefaultMode == "stopwatch") {
424 | timer = setDriftlessInterval(() => updateStopwatch(), 1000);
425 | }
426 | // default mode: pomodoro timer
427 | else if (logseq.settings.DefaultMode == "pomodoro timer") {
428 | timer = setDriftlessInterval(() => updatePomoTimer(), 1000);
429 | }
430 |
431 | // "ding" sound and system notification
432 | if (pomo_notif_active && system_notif_active) {
433 | notifSoundVolume();
434 | pomo_notif = setDriftlessInterval(() => notif_sound.play(), (pomo_duration * 60000));
435 | system_notif = setDriftlessInterval(() => new Notification(`${duration_tracked} has passed`), (pomo_duration * 60000));
436 | }
437 | else if (pomo_notif_active && !system_notif_active) {
438 | notifSoundVolume();
439 | pomo_notif = setDriftlessInterval(() => notif_sound.play(), (pomo_duration * 60000));
440 | }
441 | else if (!pomo_notif_active && system_notif_active) {
442 | system_notif = setDriftlessInterval(() => new Notification(`${duration_tracked} has passed`), (pomo_duration * 60000));
443 | }
444 |
445 | // removes timer mode and shows the stopwatch emoji next to the task
446 | logseq.provideStyle(`
447 | #timer-mode {
448 | display: none;
449 | }
450 | #block-content-${selected_task_uuid} > .flex.flex-row.justify-between > .flex-1 > span.inline::after {
451 | content: "${logseq.settings.ActiveTaskEmoji}";
452 | padding-left: 0.5em;
453 | font-size: 1.1em;
454 | }
455 | `);
456 | }
457 |
458 | function stopTimerBasics() {
459 | pause = true;
460 | clearDriftless(timer);
461 | clearDriftless(pomo_notif);
462 | clearDriftless(system_notif);
463 | logseq.hideMainUI();
464 | app.style.display = "none";
465 | add_task.style.display = "block";
466 | add_task.value = "";
467 | add_task.placeholder = 'Add a new task';
468 | stop_button.style.display = "none";
469 | start_button.style.display = "block";
470 | log_entry_stop = format(new Date(), "yyyy-MM-dd EEE HH:mm:ss");
471 |
472 | // shows timer mode and removes the stopwatch emoji next to the task
473 | logseq.provideStyle(`
474 | #timer-mode {
475 | display: block;
476 | }
477 | #block-content-${selected_task_uuid} > .flex.flex-row.justify-between > .flex-1 > span.inline::after {
478 | content: "";
479 | }
480 | `);
481 | }
482 |
483 | function startTimer(e) {
484 | selected_task_uuid = e.uuid;
485 | startTimerBasics();
486 |
487 | logseq.Editor.getBlock(e.uuid).then(selectedBlock => {
488 | block_content = selectedBlock.content;
489 | // update block w/ time-tracked property if the property doesn't exist
490 | if (block_content.includes("time-tracked::") == false) {
491 | logseq.Editor.upsertBlockProperty(selectedBlock.uuid, "time-tracked", "00:00:00");
492 | }
493 | else {
494 | block_content = block_content.split("time-tracked::")[0];
495 | }
496 | app.textContent = block_content;
497 | });
498 | }
499 |
500 | function stopTimer(e) {
501 | stopTimerBasics();
502 |
503 | logseq.Editor.getBlock(e.uuid).then(thisBlock => {
504 | logseq.Editor.getBlockProperty(thisBlock.uuid, "time-tracked").then(time => {
505 | // new task
506 | if (time == "00:00:00") {
507 | // updates the block w/ the log entry
508 | if (logseq.settings.Logs) {
509 | block_content = `${block_content}\n\n:LOG-ENTRIES:\nCLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`;
510 | logseq.Editor.updateBlock(thisBlock.uuid, block_content);
511 | }
512 |
513 | setDriftlessTimeout(() => {
514 | // updates time-tracked property
515 | logseq.Editor.upsertBlockProperty(thisBlock.uuid, "time-tracked", `${duration_tracked}`);
516 | }, 25);
517 | }
518 | // existing task w/ previously tracked time
519 | else {
520 | // 00:00:00
521 | h = parseInt(time.split(":")[0]) * 3600; // hour
522 | m = parseInt(time.split(":")[1]) * 60; // minute
523 | s = parseInt(time.split(":")[2]); // second
524 | addTimeTracked();
525 |
526 | // updates the block w/ the log entry
527 | if (logseq.settings.Logs) {
528 | // if the block content contains ":LOG-ENTRIES:"
529 | if (thisBlock.content.includes(":LOG-ENTRIES:")) {
530 | block_content = (thisBlock.content).split(":END:")[0];
531 | logseq.Editor.updateBlock(thisBlock.uuid, `${block_content}CLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`);
532 | }
533 | // if the block content doesn't contain ":LOG-ENTRIES:"
534 | else {
535 | logseq.Editor.updateBlock(thisBlock.uuid, `${block_content}\n:LOG-ENTRIES:\nCLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`);
536 | }
537 | }
538 |
539 | // updates the time-tracked property
540 | setDriftlessTimeout(() => {
541 | logseq.Editor.upsertBlockProperty(thisBlock.uuid, "time-tracked", `${hours}:${minutes}:${seconds}`);
542 | }, 25);
543 | }
544 | });
545 | });
546 |
547 | // removes stopwatch: duration (if displayed)
548 | logseq.provideUI ({
549 | key: "stopwatch-duration",
550 | path: "#toolbar-duration",
551 | template: ``
552 | });
553 |
554 | // removes pomodoro timer: duration (if displayed)
555 | logseq.provideUI ({
556 | key: "pomoTimer-duration",
557 | path: "#toolbar-duration",
558 | template: ``
559 | });
560 | }
561 |
562 | function updateStopwatch() {
563 | current_time = getUnixTime(new Date());
564 | time_elapsed_stopwatch = (current_time - start_time);
565 | stopwatch_hr_duration = Math.floor(time_elapsed_stopwatch/3600);
566 | stopwatch_min_duration = Math.floor((time_elapsed_stopwatch/60) % 60);
567 | stopwatch_sec_duration = time_elapsed_stopwatch % 60;
568 |
569 | if ((Number.isInteger(start_time)) && (pause == false)) {
570 | // 00:00:seconds
571 | if (time_elapsed_stopwatch <= 59) {
572 | stopwatch_sec_duration = (stopwatch_sec_duration.toString().length == 1) ? `0${stopwatch_sec_duration}` : `${stopwatch_sec_duration}`;
573 |
574 | duration = `00:00:${stopwatch_sec_duration}`;
575 | duration_tracked = duration;
576 | }
577 |
578 | // 00:minutes:seconds
579 | else if ((time_elapsed_stopwatch >= 60) && (time_elapsed_stopwatch <= 3599)) {
580 | // minutes
581 | stopwatch_min_duration = (stopwatch_min_duration.toString().length == 1) ? `0${stopwatch_min_duration}` : `${stopwatch_min_duration}`;
582 |
583 | // seconds
584 | stopwatch_sec_duration = (stopwatch_sec_duration.toString().length == 1) ? `0${stopwatch_sec_duration}` : `${stopwatch_sec_duration}`;
585 |
586 | duration = `00:${stopwatch_min_duration}:${stopwatch_sec_duration}`;
587 | duration_tracked = duration;
588 | }
589 |
590 | // hours:minutes:seconds
591 | else if ((time_elapsed_stopwatch >= 3600) && (time_elapsed_stopwatch <= 86,399)) {
592 | // hours
593 | stopwatch_hr_duration = (stopwatch_hr_duration.toString().length == 1) ? `0${stopwatch_hr_duration}` : `${stopwatch_hr_duration}`;
594 |
595 | // minutes
596 | if (stopwatch_min_duration == 0) {
597 | stopwatch_min_duration = "00";
598 | }
599 | else {
600 | stopwatch_min_duration = (stopwatch_min_duration.toString().length == 1) ? `0${stopwatch_min_duration}` : `${stopwatch_min_duration}`;
601 | }
602 |
603 | // seconds
604 | if (stopwatch_sec_duration == 0) {
605 | stopwatch_sec_duration = "00";
606 | }
607 | else {
608 | stopwatch_sec_duration = (stopwatch_sec_duration.toString().length == 1) ? `0${stopwatch_sec_duration}` : `${stopwatch_sec_duration}`;
609 | }
610 |
611 | duration = `${stopwatch_hr_duration}:${stopwatch_min_duration}:${stopwatch_sec_duration}`;
612 | duration_tracked = duration;
613 | }
614 |
615 | // if timer is running for 24 hours
616 | else {
617 | duration = "ERROR (24 hr limit)";
618 | }
619 | }
620 |
621 | // inserts stopwatch duration next to toolbar icon
622 | logseq.provideUI ({
623 | key: "stopwatch-duration",
624 | path: "#toolbar-duration",
625 | template: `${duration}`
626 | });
627 | }
628 |
629 | function updatePomoTimer() {
630 | if (!pause) {
631 | if (pomo_break == false) {
632 | // increment time_elapsed_pomo to be formatted for and displayed in the time-tracked property
633 | time_elapsed_pomo++;
634 | }
635 | else {
636 | time_elapsed_pomo = 0;
637 | }
638 |
639 | // decrement pomo_interval to be formatted for and displayed in the toolbar
640 | pomo_interval--;
641 |
642 | // pomodoro timer: hours_duration
643 | pomo_hr_duration = Math.floor(pomo_interval/3600);
644 | pomo_hr_duration = (pomo_hr_duration.toString().length == 1) ? `0${pomo_hr_duration}`: `${pomo_hr_duration}`;
645 | // pomodoro timer: seconds_duration
646 | pomo_sec_duration = pomo_interval % 60;
647 | pomo_sec_duration = (pomo_sec_duration.toString().length == 1) ? `0${pomo_sec_duration}` : `${pomo_sec_duration}`;
648 | // pomodoro timer: hours_tracked
649 | pomo_hr_tracked = Math.floor(time_elapsed_pomo/3600);
650 | pomo_hr_tracked = (pomo_hr_tracked.toString().length == 1) ? `0${pomo_hr_tracked}` : `${pomo_hr_tracked}`;
651 | // pomodoro timer: seconds_tracked
652 | pomo_sec_tracked = time_elapsed_pomo % 60;
653 | pomo_sec_tracked = (pomo_sec_tracked.toString().length == 1) ? `0${pomo_sec_tracked}` : `${pomo_sec_tracked}`;
654 |
655 | // if the duration of the pomodoro interval is less than an hour
656 | if ((pomo_duration <= 59) && (pomo_interval > -1)) {
657 | // pomodoro timer: minutes_duration
658 | pomo_min_duration = Math.floor(pomo_interval/60);
659 | pomo_min_duration = (pomo_min_duration.toString().length == 1) ? `0${pomo_min_duration}` : `${pomo_min_duration}`;
660 | // format the duration displayed in the toolbar
661 | duration = `${pomo_hr_duration}:${pomo_min_duration}:${pomo_sec_duration}`;
662 |
663 | // pomodoro timer: minutes_tracked
664 | pomo_min_tracked = Math.floor(time_elapsed_pomo/60);
665 | pomo_min_tracked = (pomo_min_tracked.toString().length == 1) ? `0${pomo_min_tracked}` : `${pomo_min_tracked}`;
666 | // format the duration displayed in the time-tracked property
667 | duration_tracked = `${pomo_hr_tracked}:${pomo_min_tracked}:${pomo_sec_tracked}`;
668 |
669 | // inserts pomodoro timer duration next to toolbar icon
670 | logseq.provideUI ({
671 | key: "pomoTimer-duration",
672 | path: "#toolbar-duration",
673 | template: `${duration}`
674 | });
675 | }
676 | // if the duration of the pomodoro interval is greater than an hour
677 | else if ((pomo_duration >= 60) && (pomo_interval > -1)) {
678 | // pomodoro timer: minutes_duration
679 | pomo_min_duration = Math.floor((pomo_interval % 3600)/60);
680 | pomo_min_duration = (pomo_min_duration.toString().length == 1) ? `0${pomo_min_duration}` : `${pomo_min_duration}`;
681 | // format the duration displayed in the toolbar
682 | duration = `${pomo_hr_duration}:${pomo_min_duration}:${pomo_sec_duration}`;
683 |
684 | // pomodoro timer: minutes_tracked
685 | pomo_min_tracked = Math.floor((time_elapsed_pomo % 3600)/60);
686 | pomo_min_tracked = (pomo_min_tracked.toString().length == 1) ? `0${pomo_min_tracked}` : `${pomo_min_tracked}`;
687 | // format the duration displayed in the time-tracked property
688 | duration_tracked = `${pomo_hr_tracked}:${pomo_min_tracked}:${pomo_sec_tracked}`;
689 |
690 | // inserts pomodoro timer duration next to toolbar icon
691 | logseq.provideUI ({
692 | key: "pomoTimer-duration",
693 | path: "#toolbar-duration",
694 | template:
695 | ``
698 | });
699 | }
700 | // if there's no time left to count down
701 | else if (pomo_interval == -1) {
702 | pomo_end = true;
703 |
704 | if (logseq.settings.AfterPomoEnd == "break, then start another timer") {
705 | if (pomo_break == false) {
706 | // get task name
707 | task_name = app.textContent;
708 |
709 | // stop pomo timer
710 | setDriftlessTimeout(() => {
711 | stop_button.click();
712 | }, 25);
713 |
714 | // add task to the plugin UI and set the interval to the break duration
715 | setDriftlessTimeout(() => {
716 | add_task.value = task_name;
717 | pomo_interval = (logseq.settings.PomoBreakDuration * 60);
718 | }, 50);
719 |
720 | setDriftlessTimeout(() => {
721 | // start the timer for the break
722 | start_button.click();
723 |
724 | // preparation to start the next timer
725 | pomo_break = true;
726 |
727 | logseq.App.showMsg("Break started");
728 | }, 100);
729 | }
730 |
731 | else {
732 | task_name = app.textContent;
733 |
734 | setDriftlessTimeout(() => {
735 | stop_button.click();
736 |
737 | // removes pomodoro timer: duration
738 | logseq.provideUI ({
739 | key: "pomoTimer-duration",
740 | path: "#toolbar-duration",
741 | template: ``
742 | });
743 | }, 25);
744 |
745 | setDriftlessTimeout(() => {
746 | add_task.value = task_name;
747 | pomo_interval = (pomo_duration * 60);
748 | }, 50);
749 |
750 | setDriftlessTimeout(() => {
751 | // start another the timer for the task
752 | start_button.click();
753 |
754 | // preparation to start the next break
755 | pomo_break = false;
756 |
757 | logseq.App.showMsg("Break ended");
758 | }, 100);
759 | }
760 | }
761 |
762 | else if (logseq.settings.AfterPomoEnd == "stop timer") {
763 | pause = true;
764 |
765 | setDriftlessTimeout(() => {
766 | stop_button.click();
767 | }, 25);
768 | }
769 | }
770 | }
771 | }
772 |
773 | const main = async () => {
774 | console.log("logseq-time-tracker-plugin loaded");
775 |
776 | // for refresh button
777 | const refresh_button_color = logseq.settings.RefreshButtonColor;
778 | const refresh_button_margin_top = logseq.settings.RefreshButtonMarginTop;
779 |
780 | logseq.App.onMacroRendererSlotted(async ({slot, payload}) => {
781 | let [renderer] = payload.arguments;
782 |
783 | if (renderer.startsWith(":refreshTotalTimeTracked")) {
784 | // add the refresh button next to the time-tracked-total property
785 | logseq.provideUI({
786 | key: slot,
787 | slot,
788 | path: `div[id^='ls-block'][id$='${payload.uuid}']`,
789 | template:
790 | `
791 |
796 | `,
797 | reset: true
798 | });
799 |
800 | // style the refresh button
801 | logseq.provideStyle(`
802 | a.button.refresh {
803 | opacity: 0.75 !important;
804 | height: 0 !important;
805 | padding: 0 !important;
806 | margin-left: 0.15em !important;
807 | }
808 | a.button.refresh.active, a.button.refresh:hover {
809 | opacity: 1 !important;
810 | background: transparent;
811 | }
812 | #icon {
813 | stroke: ${refresh_button_color};
814 | margin-top: ${refresh_button_margin_top};
815 | }
816 | `)
817 | }
818 | });
819 |
820 | // get pomodoro timer duration
821 | if (logseq.settings.PomoDuration != "") {
822 | pomo_duration = logseq.settings.PomoDuration;
823 | }
824 |
825 | // pomo notif and duration settings
826 | if ((logseq.settings.PomoNotification_Plugin) && (logseq.settings.PomoDuration != "")) {
827 | pomo_duration = logseq.settings.PomoDuration;
828 | pomo_notif_active = true;
829 | }
830 | else {
831 | pomo_notif_active = false;
832 | }
833 |
834 | // system notif settings
835 | if ((logseq.settings.PomoNotification_System) && (logseq.settings.PomoDuration != "")) {
836 | pomo_duration = logseq.settings.PomoDuration;
837 | system_notif_active = true;
838 | }
839 | else {
840 | system_notif_active = false;
841 | }
842 |
843 | // if timer is running, prevent the window from being refreshed and show a message
844 | window.addEventListener("beforeunload", function (e) {
845 | if (pause == false) {
846 | e.returnValue = false;
847 | logseq.App.showMsg("Don't forget to stop the timer", "warning");
848 | }
849 | });
850 |
851 | function darkMode() {
852 | document.getElementById("container").setAttribute("style", "background-color:#191919");
853 | add_task.setAttribute("style", "background-color:#191919; color:#CDCDCD");
854 | add_task.addEventListener("focus", () => {
855 | add_task.setAttribute("style", "background-color:#191919; color:#CDCDCD; outline:2px solid #3B3B3B;");
856 | });
857 | add_task.addEventListener("blur", () => {
858 | add_task.setAttribute("style", "background-color:#191919; color:#CDCDCD; outline:none;");
859 | });
860 | app.setAttribute("style", "background-color:#191919; color:#CDCDCD; border:2px solid #3B3B3B");
861 | }
862 |
863 | function lightMode() {
864 | document.getElementById("container").setAttribute("style", "background-color:#FFFFFF");
865 | add_task.setAttribute("style", "background-color:#FFFFFF; color:#6B6B6B;");
866 | add_task.addEventListener("focus", () => {
867 | add_task.setAttribute("style", "background-color:#FFFFFF; color:#6B6B6B; outline:2px solid #D1D1D1;");
868 | });
869 | add_task.addEventListener("blur", () => {
870 | add_task.setAttribute("style", "background-color:#FFFFFF; color:#6B6B6B; outline:none;");
871 | });
872 | app.setAttribute("style", "background-color:#FFFFFF; color:#6B6B6B; border:2px solid #D1D1D1");
873 | }
874 |
875 | logseq.App.getUserConfigs().then(configs => {
876 | // formats date in user's preferred date format
877 | todays_journal_title = format(new Date(), configs.preferredDateFormat);
878 |
879 | // adds workflow in user's preferred workflow format
880 | workflow_format = (configs.preferredWorkflow).toUpperCase();
881 |
882 | // dark/light mode based on user's preferred theme mode
883 | if (configs.preferredThemeMode == "dark") {
884 | darkMode();
885 | }
886 | else {
887 | lightMode();
888 | }
889 |
890 | logseq.App.onThemeModeChanged((updated_theme) => {
891 | if (updated_theme.mode == "dark") {
892 | darkMode();
893 | }
894 | else {
895 | lightMode();
896 | }
897 | })
898 | });
899 |
900 | // shows plugin settings for user configuration
901 | settings_button.addEventListener("click", function () {
902 | logseq.showSettingsUI();
903 | });
904 |
905 | // expands the textarea dynamically
906 | add_task.addEventListener("input", function () {
907 | this.style.height = "";
908 | this.style.height = (this.scrollHeight - 4) + "px";
909 | });
910 |
911 | add_task.addEventListener("keydown", (e) => {
912 | // cmd enter to click add task button
913 | if (e.key == "Enter" && (e.metaKey)) {
914 | add_task.blur();
915 | start_button.click();
916 | }
917 |
918 | // auto-completes brackets
919 | else if (e.key == "[") {
920 | e.preventDefault();
921 | textFieldEdit.wrapSelection(add_task, "[", "]");
922 | }
923 |
924 | // auto-completes parentheses
925 | else if (e.key == "(") {
926 | e.preventDefault();
927 | textFieldEdit.wrapSelection(add_task, "(", ")");
928 | }
929 | });
930 |
931 | start_button.addEventListener("click", function () {
932 | if ((add_task.value != "") || (app.textContent != "")) {
933 | if (logseq.settings.Workflow) {
934 | block_content = `${workflow_format} ${add_task.value}`;
935 | app.textContent = block_content;
936 | }
937 | else {
938 | block_content = `${add_task.value}`;
939 | app.textContent = block_content;
940 | }
941 |
942 | // if a page is included in the task description, add the task to that page
943 | // ref: https://stackoverflow.com/questions/24040965/regular-expression-double-brackets-text
944 | let task_page = add_task.value.match(/(?:(^|[^\[])\[\[)([^\]]*?)(?:\]\](?:($|[^\]])))/);
945 |
946 | if ((task_page) && (logseq.settings.AddtoPage)) {
947 | // check if the page exists
948 | logseq.DB.datascriptQuery(`[
949 | :find (pull ?p [*])
950 | :where
951 | [?p :block/page ?h]
952 | [?h :block/original-name "${task_page[2]}"]
953 | ]`).then(page => {
954 | // if the page exists
955 | if (page[0]) {
956 | startTimerBasics();
957 |
958 | logseq.Editor.getPageBlocksTree(task_page[2]).then(page_blocks => {
959 | last_block_uuid = page_blocks[page_blocks.length-1].uuid;
960 |
961 | logseq.Editor.insertBlock(last_block_uuid, `${block_content}\ntime-tracked:: 00:00:00`, {
962 | before: false,
963 | sibling: true
964 | });
965 | });
966 | }
967 | // if the page doesn't exist or if the page exists, but there are no blocks in the page
968 | else {
969 | logseq.App.showMsg(`${task_page[2]} doesn't exist`, "error");
970 | }
971 | });
972 | }
973 | // otherwise, add to the journal page
974 | else {
975 | // checks whether the task exists
976 | logseq.DB.datascriptQuery(`[
977 | :find (pull ?b [*])
978 | :where
979 | [?b :block/content ?content]
980 | [(clojure.string/includes? ?content "${block_content}")]
981 | ]`).then(task => {
982 | // if task doesn't exist
983 | if (task[0] == undefined) {
984 | startTimerBasics();
985 |
986 | logseq.Editor.getPageBlocksTree(todays_journal_title.toLowerCase()).then(todays_journal_blocks => {
987 | last_block_uuid = todays_journal_blocks[todays_journal_blocks.length-1].uuid;
988 |
989 | logseq.Editor.insertBlock(last_block_uuid, block_content, {
990 | before: false,
991 | sibling: true
992 | });
993 | });
994 |
995 | setDriftlessTimeout(() => {
996 | logseq.Editor.getPageBlocksTree(todays_journal_title.toLowerCase()).then(todays_journal_blocks_updated => {
997 | created_block_uuid = todays_journal_blocks_updated[todays_journal_blocks_updated.length-1].uuid;
998 | logseq.Editor.upsertBlockProperty(created_block_uuid, "time-tracked", "00:00:00");
999 | });
1000 | }, 25);
1001 | }
1002 | // if the task does exist
1003 | else {
1004 | startTimerBasics();
1005 | }
1006 | })
1007 | }
1008 | }
1009 | else {
1010 | // prevents timer from starting if there's no text added
1011 | logseq.App.showMsg("No task added", "warning");
1012 | }
1013 | });
1014 |
1015 | stop_button.addEventListener("click", () => {
1016 | stopTimerBasics();
1017 |
1018 | if (pomo_break == false) {
1019 | // search for block based on its content to update its time-tracked property
1020 | logseq.DB.datascriptQuery(`[
1021 | :find (pull ?b [*])
1022 | :where
1023 | [?b :block/content ?content]
1024 | [(clojure.string/includes? ?content "${block_content}")]
1025 | ]`).then(result => {
1026 | block_uuid = result[0][0].uuid.$uuid$;
1027 |
1028 | logseq.Editor.getBlockProperty(block_uuid, "time-tracked").then(time_tracked_property => {
1029 | if (time_tracked_property == "00:00:00") {
1030 | // updates the block w/ the log entry
1031 | if (logseq.settings.Logs) {
1032 | logseq.Editor.updateBlock(block_uuid, `${block_content}\n\n:LOG-ENTRIES:\nCLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`);
1033 | }
1034 |
1035 | setDriftlessTimeout(() => {
1036 | // updates time-tracked property
1037 | logseq.Editor.upsertBlockProperty(block_uuid, "time-tracked", `${duration_tracked}`);
1038 | }, 25);
1039 | }
1040 | else {
1041 | // 00:00:00
1042 | h = parseInt(time_tracked_property.split(":")[0]) * 3600; // hour
1043 | m = parseInt(time_tracked_property.split(":")[1]) * 60; // minute
1044 | s = parseInt(time_tracked_property.split(":")[2]); // second
1045 | addTimeTracked();
1046 |
1047 | // updates the block w/ the log entry
1048 | if (logseq.settings.Logs) {
1049 | // if the block content does contain ":LOG-ENTRIES:"
1050 | if (result[0][0].content.includes(":LOG-ENTRIES:")) {
1051 | block_content = (result[0][0].content).split(":END:")[0];
1052 | logseq.Editor.updateBlock(block_uuid, `${block_content}CLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`);
1053 | }
1054 | // if the block content doesn't contain ":LOG-ENTRIES:"
1055 | else {
1056 | logseq.Editor.updateBlock(block_uuid, `${block_content}\n:LOG-ENTRIES:\nCLOCK: [${log_entry_start}]--[${log_entry_stop}] => ${duration_tracked}\n:END:`);
1057 | }
1058 | }
1059 |
1060 | // updates the time-tracked property
1061 | setDriftlessTimeout(() => {
1062 | logseq.Editor.upsertBlockProperty(block_uuid, "time-tracked", `${hours}:${minutes}:${seconds}`);
1063 | }, 25);
1064 | }
1065 | });
1066 | });
1067 |
1068 | // removes stopwatch: duration (if displayed)
1069 | logseq.provideUI ({
1070 | key: "stopwatch-duration",
1071 | path: "#toolbar-duration",
1072 | template: ``
1073 | });
1074 |
1075 | // removes pomodoro timer: duration (if displayed)
1076 | logseq.provideUI ({
1077 | key: "pomoTimer-duration",
1078 | path: "#toolbar-duration",
1079 | template: ``
1080 | });
1081 | }
1082 |
1083 | else if (pomo_break == true || (pause == true)) {
1084 | // removes pomodoro timer: duration
1085 | logseq.provideUI ({
1086 | key: "pomoTimer-duration",
1087 | path: "#toolbar-duration",
1088 | template: ``
1089 | });
1090 | }
1091 | });
1092 |
1093 | // use the escape key to hide the plugin UI
1094 | document.addEventListener("keydown", function (e) {
1095 | if (e.key == "Escape") {
1096 | add_task.blur();
1097 | logseq.hideMainUI();
1098 | }
1099 | });
1100 |
1101 | // clicking outside of the plugin UI hides it
1102 | document.addEventListener("click", function (e) {
1103 | if (!e.target.closest("div")) {
1104 | add_task.blur();
1105 | logseq.hideMainUI();
1106 | }
1107 | });
1108 |
1109 | logseq.provideModel({
1110 | toggle() {
1111 | logseq.toggleMainUI();
1112 | add_task.focus();
1113 | },
1114 | stop() {
1115 | stop_button.click();
1116 | },
1117 | refresh(e) {
1118 | getTotalTimeTracked(e);
1119 | },
1120 | switch_to_pomo() {
1121 | logseq.updateSettings({
1122 | DefaultMode: "pomodoro timer"
1123 | });
1124 | setDriftlessTimeout(() => {
1125 | timerMode();
1126 | }, 25);
1127 | },
1128 | switch_to_stopwatch() {
1129 | logseq.updateSettings({
1130 | DefaultMode: "stopwatch"
1131 | });
1132 | setDriftlessTimeout(() => {
1133 | timerMode();
1134 | }, 25);
1135 | }
1136 | });
1137 |
1138 | logseq.setMainUIInlineStyle({
1139 | position: "absolute",
1140 | backgroundColor: "transparent",
1141 | top: "2.5em",
1142 | boxSizing: "border-box",
1143 | display: "flex",
1144 | flexDirection: "column",
1145 | gap: "0.5em",
1146 | width: "100vw",
1147 | height: "100vh",
1148 | overflow: "auto",
1149 | zIndex: 100
1150 | });
1151 |
1152 | function timerMode() {
1153 | // toolbar item
1154 | if (logseq.settings.DefaultMode == "stopwatch") {
1155 | setDriftlessTimeout(() => {
1156 | // toolbar: icon
1157 | logseq.App.registerUIItem("toolbar", {
1158 | key:"time-tracker-plugin",
1159 | template:
1160 | `
1161 |
1168 | `
1169 | });
1170 |
1171 | // toolbar: mode (S for stopwatch)
1172 | logseq.App.registerUIItem("toolbar", {
1173 | key: "default-mode",
1174 | template:
1175 | ``
1183 | });
1184 |
1185 | // toolbar: duration
1186 | logseq.App.registerUIItem("toolbar", {
1187 | key: "time-tracker-plugin-duration",
1188 | template:
1189 | ``
1190 | });
1191 | }, 25);
1192 | }
1193 | else if (logseq.settings.DefaultMode == "pomodoro timer") {
1194 | if (logseq.settings.DefaultMode == "pomodoro timer") {
1195 | let pomo_duration_toolbar = logseq.settings.PomoDuration * 60;
1196 |
1197 | if (pomo_duration_toolbar <= 59) {
1198 | pomo_min_duration = (pomo_duration_toolbar <= 9) ? `0${pomo_duration_toolbar}` : `${pomo_duration_toolbar}`
1199 | // format the duration displayed in the toolbar
1200 | duration = `00:${pomo_min_duration}:00`;
1201 | }
1202 | else {
1203 | // pomodoro timer: hours_duration
1204 | pomo_hr_duration = Math.floor(pomo_duration_toolbar/3600);
1205 | pomo_hr_duration = (pomo_hr_duration.toString().length == 1) ? `0${pomo_hr_duration}`:`${pomo_hr_duration}`;
1206 | // pomodoro timer: minutes_duration
1207 | pomo_min_duration = Math.floor((pomo_duration_toolbar % 3600)/60);
1208 | pomo_min_duration = (pomo_min_duration.toString().length == 1) ? `0${pomo_min_duration}`:`${pomo_min_duration}`;
1209 |
1210 | // format the duration displayed in the toolbar
1211 | duration = `${pomo_hr_duration}:${pomo_min_duration}:00`;
1212 | }
1213 | }
1214 | setDriftlessTimeout(() => {
1215 | // toolbar: icon
1216 | logseq.App.registerUIItem("toolbar", {
1217 | key: "time-tracker-plugin",
1218 | template:
1219 | ``
1230 | });
1231 |
1232 | // toolbar: mode (P for pomodoro timer)
1233 | logseq.App.registerUIItem("toolbar", {
1234 | key: "default-mode",
1235 | template:
1236 | ``
1244 | });
1245 |
1246 | // toolbar: duration
1247 | logseq.App.registerUIItem("toolbar", {
1248 | key: "time-tracker-plugin-duration",
1249 | template: ``
1250 | });
1251 | }, 25);
1252 | }
1253 | }
1254 | timerMode();
1255 |
1256 | // slash command - start
1257 | logseq.Editor.registerSlashCommand("🟢 Start time tracking", async (e) => {
1258 | startTimer(e);
1259 | });
1260 |
1261 | // slash command - stop
1262 | logseq.Editor.registerSlashCommand("🔴 Stop time tracking", async (e) => {
1263 | stopTimer(e);
1264 | });
1265 |
1266 | // slash command - get total time
1267 | logseq.Editor.registerSlashCommand("🟡 Get/update total time tracked", async (e) => {
1268 | getTotalTimeTracked(e);
1269 | });
1270 |
1271 | // right click - start
1272 | logseq.Editor.registerBlockContextMenuItem("🟢 Start time tracking", async (e) => {
1273 | startTimer(e);
1274 | });
1275 |
1276 | // right click - stop
1277 | logseq.Editor.registerBlockContextMenuItem("🔴 Stop time tracking", async (e) => {
1278 | stopTimer(e);
1279 | });
1280 |
1281 | // right click - get total time
1282 | logseq.Editor.registerBlockContextMenuItem("🟡 Get/update total time tracked", async (e) => {
1283 | getTotalTimeTracked(e);
1284 | });
1285 |
1286 | let keyboard_shortcut_version = 0;
1287 |
1288 | // register keyboard shortcuts
1289 | function registerKeyboardShortcut(label, version, keyboard_shortcut) {
1290 | // start/stop timer
1291 | if (label == "KeyboardShortcut_Timer") {
1292 | logseq.App.registerCommandPalette({
1293 | key: `time-tracker-${label}-${version}`,
1294 | label: "Start/stop timer",
1295 | keybinding: {
1296 | binding: keyboard_shortcut,
1297 | mode: "global",
1298 | }
1299 | }, async () => {
1300 | if (pause) {
1301 | logseq.Editor.checkEditing().then(task_uuid => {
1302 | if (task_uuid) {
1303 | logseq.Editor.getBlock(task_uuid).then(task => {
1304 | add_task.value = task.content;
1305 | startTimer(task);
1306 | });
1307 |
1308 | logseq.Editor.exitEditingMode();
1309 | }
1310 | else {
1311 | logseq.App.showMsg("No task selected", "warning");
1312 | }
1313 | });
1314 | }
1315 | else {
1316 | stop_button.click();
1317 | }
1318 | });
1319 | }
1320 |
1321 | // get/update total time tracked
1322 | else if (label == "KeyboardShortcut_getTotalTimeTracked") {
1323 | logseq.App.registerCommandPalette({
1324 | key: `time-tracker-${label}-${version}`,
1325 | label: "Get/Update total time tracked",
1326 | keybinding: {
1327 | binding: keyboard_shortcut,
1328 | mode: "global",
1329 | }
1330 | }, async (e) => {
1331 | getTotalTimeTracked(e);
1332 | logseq.Editor.exitEditingMode();
1333 | });
1334 | }
1335 | }
1336 |
1337 | // unregister keyboard shortcut to tidy block(s)
1338 | function unregisterKeyboardShortcut(label, version) {
1339 | logseq.App.unregister_plugin_simple_command(`${logseq.baseInfo.id}/time-tracker-${label}-${version}`);
1340 |
1341 | version++;
1342 | }
1343 |
1344 | logseq.onSettingsChanged(updated_settings => {
1345 | // register keyboard shortcuts
1346 | if (keyboard_shortcut_version == 0) {
1347 | registerKeyboardShortcut("KeyboardShortcut_Timer", keyboard_shortcut_version, updated_settings.KeyboardShortcut_Timer);
1348 | registerKeyboardShortcut("KeyboardShortcut_getTotalTimeTracked", keyboard_shortcut_version, updated_settings.KeyboardShortcut_getTotalTimeTracked);
1349 |
1350 | // keyboard_shortcut_version = 0 → 1;
1351 | keyboard_shortcut_version++;
1352 | }
1353 | // when the keyboard shortcut is modified:
1354 | else {
1355 | // keyboard_shortcut_version = 1 → 0;
1356 | keyboard_shortcut_version--;
1357 |
1358 | // unregister previous shortcut
1359 | unregisterKeyboardShortcut("KeyboardShortcut_Timer", keyboard_shortcut_version);
1360 | unregisterKeyboardShortcut("KeyboardShortcut_getTotalTimeTracked", keyboard_shortcut_version);
1361 | }
1362 | });
1363 | }
1364 |
1365 | logseq.ready(main).catch(console.error);
--------------------------------------------------------------------------------