├── .gitignore ├── LICENSE ├── README.md ├── main.ts ├── manifest.json ├── obsidian-timer-log-quirk.gif ├── obsidian-timer.gif ├── package.json ├── rollup.config.js ├── styles.css ├── tsconfig.json └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2021] [David Vogel] 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Note Timer 2 | This plugin for [Obsidian](https://obsidian.md/) uses codeblocks to insert an interactive timer to your notes. 3 | 4 | ## Features 5 | Time individual notes and automatically keep an editable log of your times! 6 | ![gif](obsidian-timer.gif) 7 | 8 | 9 | ## Usage 10 | Add a code block timer to any note with the following: 11 | ````markdown 12 | ```timer 13 | 14 | ``` 15 | ```` 16 | You can adjust the features of all timers in the settings tab, but you can also fine tune individual timers : 17 | ````markdown 18 | ```timer 19 | log: true 20 | ms: true 21 | ``` 22 | ```` 23 | > NOTE: Timer specific settings always take precedence over globally set settings. 24 | 25 | Here are the available settings and their values 26 | | Setting | Values | Description | 27 | | ------- | ------ | ----------- | 28 | | `log` | `true` or `false` | If `true`, adds the log button to the timer controls. Clicking this adds a new entry to your note's timer log markdown table.
If `false`, removes the log button, and disallows logging for this timer. 29 | | `ms` | `true` or `false` | If `true`, timer displays as HH:MM:SS:sss.
If `false`, timer displays without the fast updating millesconds.
*note:* this option only changes the display, not the speed at which the timer runs. 30 | | `ms` | `true` or `false` | If `true`, timer displays as HH:MM:SS:sss.
If `false`, timer displays without the fast updating millesconds.
*note:* this option only changes the display, not the speed at which the timer runs. 31 | | `startButtonText` | text | Change the text in the Start Button | 32 | | `stopButtonText` | text | Change the text in the Stop Button | 33 | | `resetButtonText` | text | Change the text in the Reset Button | 34 | | `showResetButton` | `true` or `false` | If `true`, timer keeps running after a reset.
If `false`, the timer resets and stops without loging.
| 35 | > NOTE: As development continues, more features that are available in global settings will be added to timer specific settings 36 | 37 | 38 | ## Installation 39 | - Open up the plugins folder in you local file explorer. 40 | - You can find this in settings, under Community Plugins, on the Installed Plugins header. 41 | - Create a folder called `obsidian-note-timer` 42 | - Click the Latest Release from the Releases section and download `main.js`, `styles.css`, and `manifest.json`. 43 | - Place these files inside of the `obsidian-note-timer` folder. 44 | - Reload Obsidian 45 | - If prompted about Safe Mode, you can disable safe mode and enable the plugin. Otherwise head to Settings, third-party plugins, make sure safe mode is off and enable the plugin from there. 46 | 47 | ## For Developers 48 | This is my first plugin for Obsidian, and it has definitely been built with huge support from the community. Pull requests are welcomed and appreciated! 49 | 50 | Feel free to let me know of any issues or concerns with this plugin or if you have a solution to any of the below: 51 | ### Known Issues/Accidental Features 52 | 1. Multiple timers in one note all log to the same table 53 | ![gif](obsidian-timer-log-quirk.gif) 54 | 55 | > NOTE: bug 1 might be turned into a feature by allowing the user to declare `log-ID: Special` and then the function will search for the header `###### Special Timer Log`, but as of right now the current behavior is unexpected. 56 | 57 | ## Support 58 | Hey! Thanks for checking out my Obsidian plugin! 59 | 60 | If you like my work and want to support me going forward, please consider donating. 61 | 62 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/S6S55K9XD) 63 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { randomUUID } from 'crypto'; 3 | import { Moment } from 'moment'; 4 | import { App, MarkdownPostProcessor, MarkdownPostProcessorContext, moment, Plugin, PluginSettingTab, Setting } from 'obsidian'; 5 | 6 | interface NoteTimerSettings { 7 | autoLog: boolean; 8 | dateFormat: string; 9 | logDateLinking: string; 10 | msDisplay: boolean; 11 | startButtonText: string; 12 | stopButtonText: string; 13 | resetButtonText: string; 14 | showResetButton: boolean; 15 | continueRunningOnReset: boolean; 16 | } 17 | 18 | const DEFAULT_SETTINGS: NoteTimerSettings = { 19 | autoLog: false, 20 | dateFormat: 'YYYY-MM-DD', 21 | logDateLinking: 'none', 22 | msDisplay: true, 23 | startButtonText: 'Play', 24 | stopButtonText: 'Stop', 25 | resetButtonText: 'Reset', 26 | showResetButton: true, 27 | continueRunningOnReset: false, 28 | } 29 | 30 | interface RunningTimerSettings { 31 | id: string; 32 | startDate: null | Date; 33 | status: 'stopped' | 'running'; 34 | timer: number | null; 35 | } 36 | 37 | export default class NoteTimer extends Plugin { 38 | settings: NoteTimerSettings; 39 | timerInterval : null | number = null; 40 | 41 | nextOpenLine(positions:number[], target:number) { 42 | // target: identifies the table location 43 | // +3: next 3 line breaks are md table column titles, and format lines 44 | return positions[positions.findIndex(n => n > target)+3] 45 | } 46 | 47 | readLocalConfig(src: string, key: string) { 48 | let value = src.replace(/\r/g,'').split('\n').find(line => line.startsWith(key+':')) 49 | return value && value.replace(key+':','').trim() 50 | } 51 | 52 | isTrue(src:string, key:string, setting:boolean ) { 53 | if(['true','false'].includes(this.readLocalConfig(src,key))){ 54 | return this.readLocalConfig(src,key) == 'true' 55 | } 56 | return setting 57 | 58 | } 59 | 60 | async calculateTotalDuration(logPosition:number, ctx:MarkdownPostProcessorContext) { 61 | const actFile = this.app.vault.getFiles().find(file => file.path === ctx.sourcePath) 62 | const curString = await this.app.vault.read(actFile); 63 | 64 | let lines = curString.replace(/\r/g,'').split('\n'); 65 | 66 | let pos = 0; 67 | let logTitleLine = lines.findIndex(line => { 68 | pos += line.length+1; //new line 69 | return pos > logPosition; 70 | }); 71 | 72 | let total = 0; 73 | let line = ''; 74 | for (let index = logTitleLine+3; index < lines.length; index++) { 75 | line = lines[index] 76 | if(!line.startsWith('|')) break; 77 | 78 | let matches = line.match(/(?<=(\|[^|]+){2}\|)[^|]+/); 79 | let value = 0 80 | try { 81 | value = parseFloat(matches[0].trim()) || 0; 82 | } catch {} 83 | total += value; 84 | } 85 | const totalTimeText = '\nTotal Time: ' + total.toLocaleString('en-EN',{minimumFractionDigits:3, maximumFractionDigits:3}); 86 | 87 | return this.app.vault.modify(actFile, curString.replace(/\nTotal Time: \d+\.\d+/,totalTimeText)) 88 | } 89 | 90 | async addToTimerLog(startDate: Moment,logPosition:number, ctx:MarkdownPostProcessorContext) { 91 | 92 | const actFile = this.app.vault.getFiles().find(file => file.path === ctx.sourcePath) 93 | const curString = await this.app.vault.read(actFile); 94 | 95 | let stopDate = moment() 96 | 97 | const durationMs = stopDate.diff(startDate,'milliseconds'); 98 | 99 | const durationtext = (durationMs/(3600*1000)).toLocaleString('en-EN',{minimumFractionDigits:3, maximumFractionDigits:3}); 100 | 101 | let startDateText = startDate.format(this.settings.dateFormat) 102 | let stopDateText = stopDate.format(this.settings.dateFormat) 103 | 104 | switch (this.settings.logDateLinking) { 105 | case 'tag': 106 | startDateText = `#${startDateText}` 107 | stopDateText = `#${stopDateText}` 108 | break; 109 | case 'link': 110 | startDateText = `[[${startDateText}]]` 111 | stopDateText = `[[${stopDateText}]]` 112 | default: 113 | break; 114 | } 115 | 116 | const newLinePositions = [] 117 | 118 | for(let c = 0; c < curString.length; c++){ 119 | // creates an array of all new line positions 120 | if(curString[c] == '\n') newLinePositions.push(c); 121 | } 122 | 123 | const curStringPart1 = curString.slice(0, this.nextOpenLine(newLinePositions, logPosition)) 124 | const curStringPart2 = curString.slice(this.nextOpenLine(newLinePositions, logPosition),curString.length) 125 | const logEntry = `\n| ${startDateText} | ${stopDateText} | ${durationtext} | |` 126 | 127 | return this.app.vault.modify(actFile, curStringPart1 + logEntry + curStringPart2) 128 | } 129 | 130 | async createNewTimerLog(ctx:MarkdownPostProcessorContext) { 131 | const actFile = this.app.vault.getFiles().find(file => file.path === ctx.sourcePath) 132 | const curString = await this.app.vault.read(actFile); 133 | const timerBlockStart = curString.toLowerCase().search("```timer") 134 | const timerBlockEnd = curString.slice(timerBlockStart, curString.length).indexOf("```", 3) + 3 135 | const curStringPart1 = curString.slice(0, timerBlockStart + timerBlockEnd) 136 | const curStringPart2 = curString.slice(timerBlockStart + timerBlockEnd, curString.length) 137 | const tableStr = `\n###### Timer Log\nTotal Time: 0.000\n| Start | Stop | Duration | Comments |\n| ----- | ---- | -------- | ------- |` 138 | return this.app.vault.modify(actFile, curStringPart1 + tableStr + curStringPart2) 139 | } 140 | 141 | async saveTimerUID(ctx:MarkdownPostProcessorContext, id: string) { 142 | const actFile = this.app.vault.getFiles().find(file => file.path === ctx.sourcePath) 143 | const curString = await this.app.vault.read(actFile); 144 | const timerBlockStart = curString.toLowerCase().search("```timer") 145 | const timerBlockEnd = curString.slice(timerBlockStart, curString.length).indexOf("```", 3) 146 | const curStringPart1 = curString.slice(0, timerBlockStart + timerBlockEnd) 147 | const curStringPart2 = curString.slice(timerBlockStart + timerBlockEnd,curString.length) 148 | const idString = `_timerUID:${id}\n`; 149 | return this.app.vault.modify(actFile, curStringPart1 + idString + curStringPart2); 150 | } 151 | 152 | timers:{[key:string]:RunningTimerSettings} = {}; 153 | 154 | async onload() { 155 | await this.loadSettings(); 156 | this.addSettingTab(new NoteTimerSettingsTab(this.app, this)); 157 | 158 | this.registerMarkdownCodeBlockProcessor("timer", (src,el,ctx) => { 159 | let uid = this.readLocalConfig(src,'_timerUID'); 160 | if(!uid) { 161 | uid = randomUUID(); 162 | this.saveTimerUID(ctx, uid); 163 | this.timers[uid] = { 164 | id: uid, 165 | status: 'stopped', 166 | startDate: null, 167 | timer: null 168 | } 169 | } 170 | 171 | if(!this.timers[uid]) { 172 | this.timers[uid] = { 173 | id: uid, 174 | status: 'stopped', 175 | startDate: null, 176 | timer: null 177 | } 178 | } 179 | 180 | const currentTimer = this.timers[uid]; 181 | 182 | const updateTime = () => { 183 | 184 | if(currentTimer.status == 'stopped') return timeDisplay.setText('-:-:-'); 185 | let start = moment(currentTimer.startDate); 186 | let now = moment(); 187 | 188 | const days = now.diff(start,'days'); 189 | start.add(days,'days') 190 | const hours = now.diff(start,'hours'); 191 | start.add(hours,'hours') 192 | const minutes = now.diff(start,'minutes'); 193 | start.add(minutes,'minutes') 194 | const seconds = now.diff(start,'seconds'); 195 | start.add(seconds,'seconds') 196 | const milliseconds = now.diff(start,'milliseconds'); 197 | start.add(milliseconds,'milliseconds') 198 | function format(value:number, digits:number=2) { 199 | return String(value).padStart(digits,'0'); 200 | } 201 | timeDisplay.setText( 202 | (days > 0 ? days + ':' : '') 203 | + format(hours) 204 | + ':' + format(minutes) 205 | + ':' + format(seconds) 206 | + (this.isTrue(src, 'ms', this.settings.msDisplay) ? '.' + format(milliseconds,3) : '') 207 | ) 208 | } 209 | 210 | const timerControl = () => { 211 | if(currentTimer.status=='running') { 212 | window.clearInterval(currentTimer.timer) 213 | currentTimer.timer = window.setInterval(() => { 214 | updateTime(); 215 | }, 10); 216 | this.registerInterval(currentTimer.timer); 217 | } else { 218 | window.clearInterval(currentTimer.timer); 219 | } 220 | } 221 | 222 | const timeDisplay = el.createEl("span", { text: '-:-:-'}) 223 | 224 | const buttonDiv = el.createDiv({ cls: "timer-button-group"}) 225 | const start = buttonDiv.createEl("button", { text: this.readLocalConfig(src,'startButtonText') || this.settings.startButtonText, cls: "timer-start" }) 226 | const stop = buttonDiv.createEl("button" ,{ text: this.readLocalConfig(src,'stopButtonText') || this.settings.stopButtonText, cls: "timer-pause"}); 227 | const reset = this.isTrue(src,'showResetButton',this.settings.showResetButton) && buttonDiv.createEl("button" ,{ text: this.readLocalConfig(src,'resetButtonText') || this.settings.resetButtonText, cls: "timer-reset"}); 228 | 229 | if(currentTimer.status=='running') { 230 | timerControl(); 231 | start.disabled = true; 232 | stop.disabled = false; 233 | } else { 234 | start.disabled = false; 235 | stop.disabled = true; 236 | } 237 | 238 | start.onclick = () => { 239 | currentTimer.startDate = new Date(); 240 | currentTimer.status = 'running'; 241 | timerControl(); 242 | start.disabled = true; 243 | stop.disabled = false; 244 | } 245 | stop.onclick = async () => { 246 | let stopTime = moment(currentTimer.startDate); 247 | currentTimer.startDate = null; 248 | currentTimer.status = 'stopped'; 249 | timerControl(); 250 | timeDisplay.setText('-:-:-') 251 | start.disabled = false; 252 | stop.disabled = true; 253 | let area = ctx.getSectionInfo(el).text 254 | let logPosition = area.search("# Timer Log") 255 | if(logPosition <= 0){ 256 | await this.createNewTimerLog(ctx); 257 | area = ctx.getSectionInfo(el).text 258 | logPosition = area.search("# Timer Log") 259 | } 260 | await this.addToTimerLog(stopTime, logPosition, ctx); 261 | await this.calculateTotalDuration(logPosition, ctx); 262 | } 263 | if(reset) { 264 | reset.onclick = () => { 265 | if(this.settings.continueRunningOnReset && currentTimer.status == 'running') { 266 | currentTimer.startDate = new Date; 267 | } else { 268 | currentTimer.startDate = null; 269 | currentTimer.status = 'stopped'; 270 | timerControl(); 271 | timeDisplay.setText('-:-:-') 272 | start.disabled = false; 273 | stop.disabled = true; 274 | } 275 | } 276 | } 277 | }); 278 | 279 | } 280 | 281 | onunload() { 282 | console.log('unloading plugin'); 283 | this.timers = {}; 284 | } 285 | 286 | async loadSettings() { 287 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 288 | } 289 | 290 | async saveSettings() { 291 | await this.saveData(this.settings); 292 | } 293 | } 294 | 295 | class NoteTimerSettingsTab extends PluginSettingTab { 296 | plugin: NoteTimer; 297 | 298 | constructor(app: App, plugin: NoteTimer) { 299 | super(app, plugin); 300 | this.plugin = plugin; 301 | } 302 | 303 | display(): void { 304 | let {containerEl} = this; 305 | 306 | containerEl.empty(); 307 | 308 | containerEl.createEl('h2', {text: 'Obsidian Note Timer Settings'}); 309 | containerEl.createEl('p', { text: `Find the documentation `}).createEl('a', { text:`here`, href: `https://github.com/davidvdev/obsidian-note-timer#readme`}) 310 | 311 | new Setting(containerEl) 312 | .setName('Display Milleseconds') 313 | .setDesc('Turn off to display HH:MM:SS') 314 | .addToggle(toggle => toggle 315 | .setValue(this.plugin.settings.msDisplay) 316 | .onChange(async (value) => { 317 | this.plugin.settings.msDisplay = value 318 | await this.plugin.saveSettings() 319 | })) 320 | new Setting(containerEl) 321 | .setName('Log by default') 322 | .setDesc('Enables the log button and automatically creates a markdown table below the timer to store the date, timer duration, and an empty cell for comments.') 323 | .addToggle( toggle => toggle 324 | .setValue(this.plugin.settings.autoLog) 325 | .onChange(async (value) => { 326 | this.plugin.settings.autoLog = value; 327 | await this.plugin.saveSettings(); 328 | })); 329 | new Setting(containerEl) 330 | .setName('Log Date Format') 331 | .setDesc('select a date format') 332 | .addText(text => text 333 | .setPlaceholder(String(DEFAULT_SETTINGS.dateFormat)) 334 | .setValue(this.plugin.settings.dateFormat) 335 | .onChange(async (value) => { 336 | this.plugin.settings.dateFormat = value 337 | await this.plugin.saveSettings() 338 | })) 339 | new Setting(containerEl) 340 | .setName('Log Date Linking') 341 | .setDesc('automatically insert wikilinks, tags, or nothing to dates') 342 | .addDropdown( dropdown => dropdown 343 | .addOption('none','none') 344 | .addOption('tag','#tag') 345 | .addOption('link','[[link]]') 346 | .setValue(this.plugin.settings.logDateLinking) 347 | .onChange( async (value) => { 348 | this.plugin.settings.logDateLinking = value 349 | await this.plugin.saveSettings() 350 | })) 351 | 352 | new Setting(containerEl) 353 | .setName('Start Button Text') 354 | .setDesc('Display text for the Start button') 355 | .addText(text => text 356 | .setPlaceholder(String(DEFAULT_SETTINGS.startButtonText)) 357 | .setValue(this.plugin.settings.startButtonText) 358 | .onChange(async (value) => { 359 | this.plugin.settings.startButtonText = value 360 | await this.plugin.saveSettings() 361 | })) 362 | 363 | new Setting(containerEl) 364 | .setName('Stop Button Text') 365 | .setDesc('Display text for the Stop button') 366 | .addText(text => text 367 | .setPlaceholder(String(DEFAULT_SETTINGS.stopButtonText)) 368 | .setValue(this.plugin.settings.stopButtonText) 369 | .onChange(async (value) => { 370 | this.plugin.settings.stopButtonText = value 371 | await this.plugin.saveSettings() 372 | })) 373 | 374 | new Setting(containerEl) 375 | .setName('Reset Button Text') 376 | .setDesc('Display text for the Reset button') 377 | .addText(text => text 378 | .setPlaceholder(String(DEFAULT_SETTINGS.resetButtonText)) 379 | .setValue(this.plugin.settings.resetButtonText) 380 | .onChange(async (value) => { 381 | this.plugin.settings.resetButtonText = value 382 | await this.plugin.saveSettings() 383 | })) 384 | 385 | new Setting(containerEl) 386 | .setName('Continue running on reset') 387 | .setDesc('If this is active, the timer will keep running after a reset.') 388 | .addToggle(toggle => toggle 389 | .setValue(this.plugin.settings.continueRunningOnReset) 390 | .onChange(async (value) => { 391 | this.plugin.settings.continueRunningOnReset = value; 392 | await this.plugin.saveSettings(); 393 | })); 394 | 395 | new Setting(containerEl) 396 | .setName('Donate') 397 | .setDesc('If you like this Plugin, please consider donating:') 398 | .addButton( button => button 399 | .buttonEl.outerHTML = `Buy Me a Coffee at ko-fi.com` 400 | )} 401 | } 402 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-note-timer", 3 | "name": "Note Timer", 4 | "version": "0.2.0", 5 | "minAppVersion": "0.9.12", 6 | "description": "Use codeblocks to add a timer and optional log to your notes.", 7 | "author": "davidvdev", 8 | "authorUrl": "https://www.davidvdev.com", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /obsidian-timer-log-quirk.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidvdev/obsidian-note-timer/52b79e190d5ea99f4b853cd9496c20b935fd384d/obsidian-timer-log-quirk.gif -------------------------------------------------------------------------------- /obsidian-timer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidvdev/obsidian-note-timer/52b79e190d5ea99f4b853cd9496c20b935fd384d/obsidian-timer.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-note-timer", 3 | "version": "0.2.0", 4 | "description": "Use codeblocks to add a timer and optional log to your notes.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "davidvdev", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.2.1", 17 | "@types/node": "^14.14.37", 18 | "obsidian": "^0.12.0", 19 | "rollup": "^2.32.1", 20 | "tslib": "^2.2.0", 21 | "typescript": "^4.2.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .block-language-timer { 2 | background-color: rgba(0,0,0,0.75); 3 | height: 2em; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 3em; 8 | margin: 0; 9 | color: white; 10 | } 11 | 12 | .block-language-timer timer-button-group { 13 | display: flex; 14 | justify-content: space-around; 15 | } 16 | 17 | .block-language-timer button { 18 | margin: 0 0.5em; 19 | padding: 1em; 20 | } 21 | 22 | .block-language-timer span { 23 | display: inline-block; 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "0.9.12", 3 | "1.0.0": "0.9.7" 4 | } 5 | --------------------------------------------------------------------------------