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: {
 | 
			
		||||
		js: banner,
 | 
			
		||||
	},
 | 
			
		||||
	entryPoints: ["main.ts"],
 | 
			
		||||
	entryPoints: ["src/main.ts"],
 | 
			
		||||
	bundle: true,
 | 
			
		||||
	external: [
 | 
			
		||||
		"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",
 | 
			
		||||
		"tslib": "2.4.0",
 | 
			
		||||
		"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
 | 
			
		||||
available in the app when your plugin is enabled.
 | 
			
		||||
/* Pomodoro View Container */
 | 
			
		||||
.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