I hope this is the right place to ask, if it's not, please tell me where I should post it.
I'm making a pomodoro timer browser extension using WXT and Svelte.js with Typescript. My extension works quite as it should, the problem is that certain things only really update when I close and reopen the extension. It might be that I'm misusing background.ts a bit. I tried lots of different stuff, placing some events on different places, tried normal functions and async on some places but it all didn't help... Could someone give me a few tips how I make this extension dynamic and make it work properly? I'm already stuck for quite some time now...
Here's my code for Start.svelte (element for start/pause button), TimerType.svelte (to change timers, the timer count does not update dynamically) and background.ts.
<!-- Start.svelte --><script lang="ts"> import { POMODORO as pomodoro, SHORT_BREAK as shortBreak, LONG_BREAK as longBreak, } from "@/utils/constants"; import pauseIcon from "~/assets/icon/pause.png"; import playIcon from "~/assets/icon/play.png"; import timeUp from "~/assets/sound/time-up.wav"; import toDoubleDigit from "@/utils/toDoubleDigit"; import { onMount } from "svelte"; const timeUpSound = new Audio(timeUp); export let buttonState: "START" | "PAUSE" = "START"; export let timer: HTMLElement | null = document.getElementById("timer"); export let timerType: "POMODORO" | "SHORT_BREAK" | "LONG_BREAK" = "POMODORO"; export let completedSessions = { completedPomodoros: 0, completedShortBreaks: 0, completedLongBreaks: 0, }; let timeBetween: number = 0; let shouldUpdateTimer = true; onMount(async () => { const state = await browser.runtime.sendMessage({ type: "GET_STATE" }); if (state) { timerType = state.timerType; completedSessions = state.completedSessions; } initializeTimer(timerType); }); const initializeTimer = (timerType: "POMODORO" | "SHORT_BREAK" | "LONG_BREAK") => { switch (timerType) { case "POMODORO": timeBetween = Number(pomodoro); break; case "SHORT_BREAK": timeBetween = Number(shortBreak); break; case "LONG_BREAK": timeBetween = Number(longBreak); break; } if (timer) { const { minutes, seconds } = getMinutesSeconds(timeBetween); timer.innerHTML = `${minutes}:${seconds}`; } console.log(`debug> initializeTimer called with timerType: ${timerType}, timeBetween: ${timeBetween}`); }; const resetTimer = () => { switch (timerType) { case "POMODORO": timeBetween = Number(pomodoro); buttonState = "START"; break; case "SHORT_BREAK": timeBetween = Number(shortBreak); buttonState = "START"; break; case "LONG_BREAK": timeBetween = Number(longBreak); buttonState = "START"; break; } if (timer) { const { minutes, seconds } = getMinutesSeconds(timeBetween); timer.innerHTML = `${minutes}:${seconds}`; } }; initializeTimer(timerType); console.log(`debug> timeBetween: ${timeBetween}`); $: { browser.runtime.onMessage.addListener((message, sender, onResponse) => { console.log(`debug> received message inside Start.svelte: ${message.type}`); if (message.type === "RESET_TIMER") { timeUpSound.play(); completedSessions = message.completedSessions; resetTimer(); } else if (message.type === "INIT_TIMER") { initializeTimer(message.timerType); } else if (message.type === "UPDATE_TIMER") { if (timer) { timer.innerText = message.time; timeBetween = message.time .split(":") .reduce((acc: number, time: number, index: number) => { if (index === 0) { return acc + Number(time) * 60000; } else { return acc + Number(time) * 1000; } }, 0); } } }); } const getMinutesSeconds = (time: number) => ({ minutes: toDoubleDigit(Math.floor((time / 60000) % 60)), seconds: toDoubleDigit(Math.floor((time / 1000) % 60)), }); $: ({ minutes, seconds } = getMinutesSeconds(timeBetween)); const changeButtonState = () => { buttonState = buttonState === "START" ? "PAUSE" : "START"; }; $: { if (shouldUpdateTimer && timer) { timer.innerHTML = `${minutes}:${seconds}`; shouldUpdateTimer = false; } } const handleClick = async () => { if (buttonState === "START") { await browser.runtime.sendMessage({ type: "START_TIMER", time: timeBetween, }); } else { const response = await browser.runtime.sendMessage({ type: "PAUSE_TIMER", time: timeBetween, }); if (timer && response) { const time = Number(response.time); if (!isNaN(time)) { const { minutes, seconds } = getMinutesSeconds(time); timer.innerText = `${minutes}:${seconds}`; } } } changeButtonState(); };</script><button id="timerButton" on:click={handleClick}> // by the way, these buttons work correctly when you click on them, just usually don't show up correctly in the extension {#if buttonState === "START"}<img src={playIcon} width="12" alt="Play" /> {:else}<img src={pauseIcon} width="12" alt="Pause" /> {/if}</button>
<!-- TimerType.svelte --><script lang="ts"> import './timertype.css'; import './Start.svelte'; export let timerType: "POMODORO" | "SHORT_BREAK" | "LONG_BREAK" = "POMODORO"; export let buttonState: "START" | "PAUSE" = "START"; export let completedSessions = { completedPomodoros: 0, completedShortBreaks: 0, completedLongBreaks: 0 }; const handleClick = () => { const pomodoro = document.getElementById('pomodoro') as HTMLInputElement; const shortBreak = document.getElementById('shortBreak') as HTMLInputElement; const longBreak = document.getElementById('longBreak') as HTMLInputElement; if (pomodoro.checked) { timerType = "POMODORO"; } else if (shortBreak.checked) { timerType = "SHORT_BREAK"; } else if (longBreak.checked) { timerType = "LONG_BREAK"; } browser.runtime.sendMessage({ type: "INIT_TIMER", timerType }); console.log(`debug> completedSessions: ${JSON.stringify(completedSessions)}`); console.log(`debug> timer changed to: ${timerType}`); }</script><div class="timer-type" data-is="multiswitch"><form><label><input type="radio" id="pomodoro" data-id="1" name="timerType" checked on:click={handleClick} disabled={buttonState === "PAUSE"}><span>Pomodoro {#if completedSessions.completedPomodoros > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessions.completedPomodoros})</strong>{/if}</span></label><label><input type="radio" id="shortBreak" data-id="2" name="timerType" on:click={handleClick} disabled={buttonState === "PAUSE"}><span>Short Break {#if completedSessions.completedShortBreaks > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessions.completedShortBreaks})</strong>{/if}</span></label><label><input type="radio" id="longBreak" name="timerType" data-id="3" on:click={handleClick} disabled={buttonState === "PAUSE"}><span>Long Break {#if completedSessions.completedLongBreaks > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessions.completedLongBreaks})</strong>{/if}</span></label><div id="indicator"></div></form></div>
// background.tsimport { countdown } from "@/utils/countdown";import toDoubleDigit from "@/utils/toDoubleDigit";export let timerType: "POMODORO" | "SHORT_BREAK" | "LONG_BREAK" = "POMODORO";export let completedSessions = { completedPomodoros: 0, completedShortBreaks: 0, completedLongBreaks: 0,};let interval: NodeJS.Timeout | null = null;let timeBetween: number;export default defineBackground(() => { console.log("info> started StudyMate", { id: browser.runtime.id }); const getMinutesSeconds = (time: number) => { const minutes = toDoubleDigit(Math.floor(time / 60000) % 60); const seconds = toDoubleDigit(Math.floor(time / 1000) % 60); return { minutes, seconds }; }; const updateTimer = (time: number) => { let { minutes, seconds } = getMinutesSeconds(time); return `${minutes}:${seconds}`; }; const playTimer = (time: number) => { interval = countdown( time, (remainingTime) => { timeBetween = remainingTime; browser.runtime.sendMessage({ type: "UPDATE_TIMER", time: updateTimer(timeBetween), }); }, () => { if (timerType === "POMODORO") { completedSessions.completedPomodoros += 1; } else if (timerType === "SHORT_BREAK") { completedSessions.completedShortBreaks += 1; } else if (timerType === "LONG_BREAK") { completedSessions.completedLongBreaks += 1; } browser.runtime.sendMessage({ type: "RESET_TIMER" }); } ); }; const pauseTimer = () => { if (interval !== null) clearInterval(interval); }; browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "GET_STATE") { sendResponse({ timerType, completedSessions }); } else if (message.type === "START_TIMER") { playTimer(message.time); sendResponse({ status: "timerStarted", time: updateTimer(message.time) }); } else if (message.type === "PAUSE_TIMER") { pauseTimer(); console.log( `debug> background.ts sent response w/ ${updateTimer(message.time)}` ); sendResponse({ status: "timerPaused", time: updateTimer(message.time) }); } else if (message.type === "INIT_TIMER") { browser.runtime.sendMessage({ type: "INIT_TIMER", timerType: message.timerType }); } console.log( `debug> received message inside background.ts: ${ message.type } with additional data: ${JSON.stringify(message)}` ); return true; });});
If you need more code to reproduce, it's on GitHub: https://github.com/ThatFrogDev/StudyMate.
Also to reproduce, a quick example: