├── .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 | 
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 | 
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 | [](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 = `
`
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 |
--------------------------------------------------------------------------------