feat: Implement Pomodoro plugin core logic, UI, settings, and daily note logging
This commit is contained in:
parent
6d09ce3e39
commit
ccb2ec8439
|
@ -15,7 +15,7 @@ const context = await esbuild.context({
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
entryPoints: ["main.ts"],
|
entryPoints: ["src/main.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
external: [
|
||||||
"obsidian",
|
"obsidian",
|
||||||
|
|
134
main.ts
134
main.ts
|
@ -1,134 +0,0 @@
|
||||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
|
||||||
|
|
||||||
// Remember to rename these classes and interfaces!
|
|
||||||
|
|
||||||
interface MyPluginSettings {
|
|
||||||
mySetting: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
||||||
mySetting: 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MyPlugin extends Plugin {
|
|
||||||
settings: MyPluginSettings;
|
|
||||||
|
|
||||||
async onload() {
|
|
||||||
await this.loadSettings();
|
|
||||||
|
|
||||||
// This creates an icon in the left ribbon.
|
|
||||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
|
||||||
// Called when the user clicks the icon.
|
|
||||||
new Notice('This is a notice!');
|
|
||||||
});
|
|
||||||
// Perform additional things with the ribbon
|
|
||||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
|
||||||
|
|
||||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
|
||||||
const statusBarItemEl = this.addStatusBarItem();
|
|
||||||
statusBarItemEl.setText('Status Bar Text');
|
|
||||||
|
|
||||||
// This adds a simple command that can be triggered anywhere
|
|
||||||
this.addCommand({
|
|
||||||
id: 'open-sample-modal-simple',
|
|
||||||
name: 'Open sample modal (simple)',
|
|
||||||
callback: () => {
|
|
||||||
new SampleModal(this.app).open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// This adds an editor command that can perform some operation on the current editor instance
|
|
||||||
this.addCommand({
|
|
||||||
id: 'sample-editor-command',
|
|
||||||
name: 'Sample editor command',
|
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
||||||
console.log(editor.getSelection());
|
|
||||||
editor.replaceSelection('Sample Editor Command');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
|
||||||
this.addCommand({
|
|
||||||
id: 'open-sample-modal-complex',
|
|
||||||
name: 'Open sample modal (complex)',
|
|
||||||
checkCallback: (checking: boolean) => {
|
|
||||||
// Conditions to check
|
|
||||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
||||||
if (markdownView) {
|
|
||||||
// If checking is true, we're simply "checking" if the command can be run.
|
|
||||||
// If checking is false, then we want to actually perform the operation.
|
|
||||||
if (!checking) {
|
|
||||||
new SampleModal(this.app).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This command will only show up in Command Palette when the check function returns true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
|
||||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
|
||||||
|
|
||||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
|
||||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
|
||||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
|
||||||
console.log('click', evt);
|
|
||||||
});
|
|
||||||
|
|
||||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
onunload() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSettings() {
|
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveSettings() {
|
|
||||||
await this.saveData(this.settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SampleModal extends Modal {
|
|
||||||
constructor(app: App) {
|
|
||||||
super(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen() {
|
|
||||||
const {contentEl} = this;
|
|
||||||
contentEl.setText('Woah!');
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
const {contentEl} = this;
|
|
||||||
contentEl.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SampleSettingTab extends PluginSettingTab {
|
|
||||||
plugin: MyPlugin;
|
|
||||||
|
|
||||||
constructor(app: App, plugin: MyPlugin) {
|
|
||||||
super(app, plugin);
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
display(): void {
|
|
||||||
const {containerEl} = this;
|
|
||||||
|
|
||||||
containerEl.empty();
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Setting #1')
|
|
||||||
.setDesc('It\'s a secret')
|
|
||||||
.addText(text => text
|
|
||||||
.setPlaceholder('Enter your secret')
|
|
||||||
.setValue(this.plugin.settings.mySetting)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.mySetting = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,5 +20,9 @@
|
||||||
"obsidian": "latest",
|
"obsidian": "latest",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"obsidian-daily-notes-interface": "^0.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,294 @@
|
||||||
|
|
||||||
|
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, WorkspaceLeaf } from 'obsidian';
|
||||||
|
import { PomodoroView, POMODORO_VIEW_TYPE } from './pomodoroView';
|
||||||
|
import { PomodoroTimer } from './timer';
|
||||||
|
import { createDailyNote, getDailyNote, getAllDailyNotes } from 'obsidian-daily-notes-interface';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
|
||||||
|
// Remember to rename these classes and interfaces!
|
||||||
|
|
||||||
|
interface PomodoroPluginSettings {
|
||||||
|
pomodoroDuration: number;
|
||||||
|
shortBreakDuration: number;
|
||||||
|
longBreakDuration: number;
|
||||||
|
cyclesBeforeLongBreak: number;
|
||||||
|
autoStartNextSession: boolean;
|
||||||
|
enableNotifications: boolean;
|
||||||
|
enableSound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: PomodoroPluginSettings = {
|
||||||
|
pomodoroDuration: 25, // minutes
|
||||||
|
shortBreakDuration: 5, // minutes
|
||||||
|
longBreakDuration: 15, // minutes
|
||||||
|
cyclesBeforeLongBreak: 4,
|
||||||
|
autoStartNextSession: false,
|
||||||
|
enableNotifications: true,
|
||||||
|
enableSound: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PomodoroPlugin extends Plugin {
|
||||||
|
settings: PomodoroPluginSettings;
|
||||||
|
timer: PomodoroTimer;
|
||||||
|
statusBarItemEl: HTMLElement;
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
this.timer = new PomodoroTimer(
|
||||||
|
this.settings.pomodoroDuration * 60,
|
||||||
|
this.settings.shortBreakDuration * 60,
|
||||||
|
this.settings.longBreakDuration * 60,
|
||||||
|
this.settings.cyclesBeforeLongBreak
|
||||||
|
);
|
||||||
|
|
||||||
|
this.statusBarItemEl = this.addStatusBarItem();
|
||||||
|
this.updateStatusBar();
|
||||||
|
|
||||||
|
this.addRibbonIcon('timer', 'Open Pomodoro Timer', () => {
|
||||||
|
this.activateView();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.registerView(
|
||||||
|
POMODORO_VIEW_TYPE,
|
||||||
|
(leaf) => new PomodoroView(leaf, this)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'pomodoro-start',
|
||||||
|
name: 'Start Pomodoro',
|
||||||
|
callback: () => this.startTimer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'pomodoro-pause',
|
||||||
|
name: 'Pause Pomodoro',
|
||||||
|
callback: () => this.pauseTimer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'pomodoro-reset',
|
||||||
|
name: 'Reset Pomodoro',
|
||||||
|
callback: () => this.resetTimer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'pomodoro-skip',
|
||||||
|
name: 'Skip Session',
|
||||||
|
callback: () => this.skipTimer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSettingTab(new PomodoroSettingTab(this.app, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onunload() {
|
||||||
|
this.timer.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async activateView() {
|
||||||
|
this.app.workspace.detachLeavesOfType(POMODORO_VIEW_TYPE);
|
||||||
|
|
||||||
|
await this.app.workspace.getRightLeaf(false).setViewState({
|
||||||
|
type: POMODORO_VIEW_TYPE,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.workspace.revealLeaf(
|
||||||
|
this.app.workspace.getLeavesOfType(POMODORO_VIEW_TYPE)[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatusBar() {
|
||||||
|
const remainingTime = this.timer.getRemainingTime();
|
||||||
|
const minutes = Math.floor(remainingTime / 60);
|
||||||
|
const seconds = remainingTime % 60;
|
||||||
|
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
const statusText = `🍅 ${timeString}`;
|
||||||
|
this.statusBarItemEl.setText(statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
this.timer.start(
|
||||||
|
(remainingTime, isPomodoro) => {
|
||||||
|
this.updateStatusBar();
|
||||||
|
// The PomodoroView will update itself via its registered interval
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
new Notice(this.timer.getIsPomodoro() ? 'Pomodoro Complete!' : 'Break Complete!');
|
||||||
|
this.logSession();
|
||||||
|
if (this.settings.autoStartNextSession) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseTimer() {
|
||||||
|
this.timer.pause();
|
||||||
|
this.updateStatusBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTimer() {
|
||||||
|
this.timer.reset();
|
||||||
|
this.updateStatusBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
skipTimer() {
|
||||||
|
this.timer.skip();
|
||||||
|
this.updateStatusBar();
|
||||||
|
if (this.settings.autoStartNextSession) {
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logSession() {
|
||||||
|
const now = moment();
|
||||||
|
const dailyNote = getDailyNote(now, getAllDailyNotes());
|
||||||
|
|
||||||
|
let fileContent = '';
|
||||||
|
if (dailyNote) {
|
||||||
|
fileContent = await this.app.vault.read(dailyNote);
|
||||||
|
} else {
|
||||||
|
const newDailyNote = await createDailyNote(now);
|
||||||
|
fileContent = await this.app.vault.read(newDailyNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionType = this.timer.getIsPomodoro() ? 'Break' : 'Pomodoro'; // Log the *completed* session type
|
||||||
|
const duration = this.timer.getIsPomodoro() ? this.settings.pomodoroDuration : (this.timer.currentCycle % this.settings.cyclesBeforeLongBreak === 0 ? this.settings.longBreakDuration : this.settings.shortBreakDuration);
|
||||||
|
const activeNote = this.app.workspace.getActiveViewOfType(MarkdownView)?.file?.basename || 'No active note';
|
||||||
|
|
||||||
|
const logEntry = `- [x] ${now.format('HH:mm')} - ${sessionType} (${duration}分) - [[${activeNote}]]`;
|
||||||
|
|
||||||
|
if (dailyNote) {
|
||||||
|
await this.app.vault.append(dailyNote, `\n${logEntry}`);
|
||||||
|
} else {
|
||||||
|
const newDailyNote = await createDailyNote(now);
|
||||||
|
await this.app.vault.append(newDailyNote, `\n${logEntry}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PomodoroSettingTab extends PluginSettingTab {
|
||||||
|
plugin: PomodoroPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: PomodoroPlugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Pomodoro Duration')
|
||||||
|
.setDesc('Duration of a pomodoro session in minutes.')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('25')
|
||||||
|
.setValue(this.plugin.settings.pomodoroDuration.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.pomodoroDuration = parseInt(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.timer = new PomodoroTimer(
|
||||||
|
this.plugin.settings.pomodoroDuration * 60,
|
||||||
|
this.plugin.settings.shortBreakDuration * 60,
|
||||||
|
this.plugin.settings.longBreakDuration * 60,
|
||||||
|
this.plugin.settings.cyclesBeforeLongBreak
|
||||||
|
);
|
||||||
|
this.plugin.resetTimer();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Short Break Duration')
|
||||||
|
.setDesc('Duration of a short break in minutes.')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('5')
|
||||||
|
.setValue(this.plugin.settings.shortBreakDuration.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.shortBreakDuration = parseInt(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.timer = new PomodoroTimer(
|
||||||
|
this.plugin.settings.pomodoroDuration * 60,
|
||||||
|
this.plugin.settings.shortBreakDuration * 60,
|
||||||
|
this.plugin.settings.longBreakDuration * 60,
|
||||||
|
this.plugin.settings.cyclesBeforeLongBreak
|
||||||
|
);
|
||||||
|
this.plugin.resetTimer();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Long Break Duration')
|
||||||
|
.setDesc('Duration of a long break in minutes.')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('15')
|
||||||
|
.setValue(this.plugin.settings.longBreakDuration.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.longBreakDuration = parseInt(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.timer = new PomodoroTimer(
|
||||||
|
this.plugin.settings.pomodoroDuration * 60,
|
||||||
|
this.plugin.settings.shortBreakDuration * 60,
|
||||||
|
this.plugin.settings.longBreakDuration * 60,
|
||||||
|
this.plugin.settings.cyclesBeforeLongBreak
|
||||||
|
);
|
||||||
|
this.plugin.resetTimer();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Cycles Before Long Break')
|
||||||
|
.setDesc('Number of pomodoro sessions before a long break.')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('4')
|
||||||
|
.setValue(this.plugin.settings.cyclesBeforeLongBreak.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.cyclesBeforeLongBreak = parseInt(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.plugin.timer = new PomodoroTimer(
|
||||||
|
this.plugin.settings.pomodoroDuration * 60,
|
||||||
|
this.plugin.settings.shortBreakDuration * 60,
|
||||||
|
this.plugin.settings.longBreakDuration * 60,
|
||||||
|
this.plugin.settings.cyclesBeforeLongBreak
|
||||||
|
);
|
||||||
|
this.plugin.resetTimer();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Auto Start Next Session')
|
||||||
|
.setDesc('Automatically start the next pomodoro or break session after the current one ends.')
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.autoStartNextSession)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.autoStartNextSession = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Notifications')
|
||||||
|
.setDesc('Display OS native notifications when a session ends.')
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.enableNotifications)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.enableNotifications = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Sound')
|
||||||
|
.setDesc('Play a sound when a session ends.')
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.enableSound)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.enableSound = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
|
||||||
|
import { ItemView, WorkspaceLeaf, setIcon } from 'obsidian';
|
||||||
|
import { PomodoroTimer } from './timer';
|
||||||
|
import PomodoroPlugin from './main';
|
||||||
|
|
||||||
|
export const POMODORO_VIEW_TYPE = 'pomodoro-view';
|
||||||
|
|
||||||
|
export class PomodoroView extends ItemView {
|
||||||
|
plugin: PomodoroPlugin;
|
||||||
|
timer: PomodoroTimer;
|
||||||
|
private timerDisplay: HTMLElement;
|
||||||
|
private startPauseButton: HTMLElement;
|
||||||
|
private resetButton: HTMLElement;
|
||||||
|
private skipButton: HTMLElement;
|
||||||
|
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: PomodoroPlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.timer = plugin.timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType(): string {
|
||||||
|
return POMODORO_VIEW_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText(): string {
|
||||||
|
return 'Pomodoro Timer';
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(): string {
|
||||||
|
return 'timer';
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen() {
|
||||||
|
const container = this.containerEl.children[1];
|
||||||
|
container.empty();
|
||||||
|
container.addClass('pomodoro-view-container');
|
||||||
|
|
||||||
|
// Timer Display
|
||||||
|
this.timerDisplay = container.createEl('div', { cls: 'pomodoro-timer-display' });
|
||||||
|
this.timerDisplay.setText('25:00'); // Initial display
|
||||||
|
|
||||||
|
// Control Buttons
|
||||||
|
const controls = container.createEl('div', { cls: 'pomodoro-controls' });
|
||||||
|
|
||||||
|
this.startPauseButton = controls.createEl('button', { cls: 'pomodoro-button' });
|
||||||
|
setIcon(this.startPauseButton, 'play');
|
||||||
|
this.startPauseButton.onclick = () => this.toggleTimer();
|
||||||
|
|
||||||
|
this.resetButton = controls.createEl('button', { cls: 'pomodoro-button' });
|
||||||
|
setIcon(this.resetButton, 'rotate-ccw');
|
||||||
|
this.resetButton.onclick = () => this.plugin.resetTimer();
|
||||||
|
|
||||||
|
this.skipButton = controls.createEl('button', { cls: 'pomodoro-button' });
|
||||||
|
setIcon(this.skipButton, 'skip-forward');
|
||||||
|
this.skipButton.onclick = () => this.plugin.skipTimer();
|
||||||
|
|
||||||
|
this.updateUI();
|
||||||
|
this.plugin.registerInterval(window.setInterval(() => this.updateUI(), 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClose() {
|
||||||
|
// Nothing to clean up.
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
const remainingTime = this.timer.getRemainingTime();
|
||||||
|
const minutes = Math.floor(remainingTime / 60);
|
||||||
|
const seconds = remainingTime % 60;
|
||||||
|
this.timerDisplay.setText(`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
|
||||||
|
|
||||||
|
if (this.timer.getIsRunning()) {
|
||||||
|
setIcon(this.startPauseButton, 'pause');
|
||||||
|
} else {
|
||||||
|
setIcon(this.startPauseButton, 'play');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTimer() {
|
||||||
|
if (this.timer.getIsRunning()) {
|
||||||
|
this.plugin.pauseTimer();
|
||||||
|
} else {
|
||||||
|
this.plugin.startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
|
||||||
|
export class PomodoroTimer {
|
||||||
|
private pomodoroDuration: number; // in seconds
|
||||||
|
private shortBreakDuration: number; // in seconds
|
||||||
|
private longBreakDuration: number; // in seconds
|
||||||
|
private cyclesBeforeLongBreak: number;
|
||||||
|
|
||||||
|
private timerId: NodeJS.Timeout | null = null;
|
||||||
|
private remainingTime: number; // in seconds
|
||||||
|
private isRunning: boolean = false;
|
||||||
|
private currentCycle: number = 0;
|
||||||
|
private isPomodoro: boolean = true; // true for pomodoro, false for break
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
pomodoroDuration: number = 25 * 60,
|
||||||
|
shortBreakDuration: number = 5 * 60,
|
||||||
|
longBreakDuration: number = 15 * 60,
|
||||||
|
cyclesBeforeLongBreak: number = 4
|
||||||
|
) {
|
||||||
|
this.pomodoroDuration = pomodoroDuration;
|
||||||
|
this.shortBreakDuration = shortBreakDuration;
|
||||||
|
this.longBreakDuration = longBreakDuration;
|
||||||
|
this.cyclesBeforeLongBreak = cyclesBeforeLongBreak;
|
||||||
|
this.remainingTime = this.pomodoroDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(onTick: (remainingTime: number, isPomodoro: boolean) => void, onComplete: () => void) {
|
||||||
|
if (this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
this.timerId = setInterval(() => {
|
||||||
|
this.remainingTime--;
|
||||||
|
onTick(this.remainingTime, this.isPomodoro);
|
||||||
|
if (this.remainingTime <= 0) {
|
||||||
|
this.stop();
|
||||||
|
onComplete();
|
||||||
|
this.nextCycle();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public pause() {
|
||||||
|
if (this.timerId) {
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.pause();
|
||||||
|
this.currentCycle = 0;
|
||||||
|
this.isPomodoro = true;
|
||||||
|
this.remainingTime = this.pomodoroDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public skip() {
|
||||||
|
this.pause();
|
||||||
|
this.nextCycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private stop() {
|
||||||
|
if (this.timerId) {
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextCycle() {
|
||||||
|
if (this.isPomodoro) {
|
||||||
|
this.currentCycle++;
|
||||||
|
if (this.currentCycle % this.cyclesBeforeLongBreak === 0) {
|
||||||
|
this.isPomodoro = false;
|
||||||
|
this.remainingTime = this.longBreakDuration;
|
||||||
|
} else {
|
||||||
|
this.isPomodoro = false;
|
||||||
|
this.remainingTime = this.shortBreakDuration;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isPomodoro = true;
|
||||||
|
this.remainingTime = this.pomodoroDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRemainingTime(): number {
|
||||||
|
return this.remainingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIsRunning(): boolean {
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIsPomodoro(): boolean {
|
||||||
|
return this.isPomodoro;
|
||||||
|
}
|
||||||
|
}
|
58
styles.css
58
styles.css
|
@ -1,8 +1,56 @@
|
||||||
/*
|
|
||||||
|
|
||||||
This CSS file will be included with your plugin, and
|
/* Pomodoro View Container */
|
||||||
available in the app when your plugin is enabled.
|
.pomodoro-view-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
If your plugin does not need CSS, delete this file.
|
/* Timer Display */
|
||||||
|
.pomodoro-timer-display {
|
||||||
|
font-size: 4em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-normal);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-variant-numeric: tabular-nums; /* Ensures monospaced numbers */
|
||||||
|
}
|
||||||
|
|
||||||
*/
|
/* Control Buttons */
|
||||||
|
.pomodoro-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pomodoro-button {
|
||||||
|
background-color: var(--background-modifier-form-field);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
color: var(--text-normal);
|
||||||
|
border-radius: var(--button-radius);
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pomodoro-button:hover {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pomodoro-button svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Page Styling (if needed, though Obsidian handles most of it) */
|
||||||
|
.setting-item-control input[type="number"] {
|
||||||
|
width: 80px; /* Adjust as needed for number inputs */
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue