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 = statepletedSessions;
}
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 = messagepletedSessions;
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 completedSessionspletedPomodoros > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessionspletedPomodoros})</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 completedSessionspletedShortBreaks > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessionspletedShortBreaks})</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 completedSessionspletedLongBreaks > 0}<strong style="font-family: 'Manrope'; line-height: 1;">({completedSessionspletedLongBreaks})</strong>{/if}</span>
</label>
<div id="indicator"></div>
</form>
</div>
// background.ts
import { 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") {
completedSessionspletedPomodoros += 1;
} else if (timerType === "SHORT_BREAK") {
completedSessionspletedShortBreaks += 1;
} else if (timerType === "LONG_BREAK") {
completedSessionspletedLongBreaks += 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: .
Also to reproduce, a quick example: