├── .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 | ![logseq-time-tracker-plugin switch timer mode demo](screenshots/logseq_time_tracker_switch_timer_mode_demo.gif) 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 | ![logseq-time-tracker-plugin main demo](screenshots/logseq_time_tracker_main_demo.gif) 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 | ![logseq-time-tracker-plugin total time tracked demo](screenshots/logseq_time_tracker_totalTimeTracked_demo.gif) 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 | ![logseq-time-tracker-plugin vs Logseq's native time tracking](screenshots/plugin_vs_native_timetracking.png) _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 | ![logseq-time-tracker-plugin settings](screenshots/logseq_time_tracker_settings.png) 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 | Buy Me A Coffee -------------------------------------------------------------------------------- /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 |
116 |
117 | 118 |
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 | `
696 | ${duration} 697 |
` 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 | 792 | 793 | 794 | 795 | 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 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | ` 1169 | }); 1170 | 1171 | // toolbar: mode (S for stopwatch) 1172 | logseq.App.registerUIItem("toolbar", { 1173 | key: "default-mode", 1174 | template: 1175 | `
1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 |
` 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 | `
1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 |
` 1230 | }); 1231 | 1232 | // toolbar: mode (P for pomodoro timer) 1233 | logseq.App.registerUIItem("toolbar", { 1234 | key: "default-mode", 1235 | template: 1236 | `
1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 |
` 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); --------------------------------------------------------------------------------