fix(achievments): Make achievements work with new IndexedDB

- currently has extremely verbose logs in console, to be removed later
This commit is contained in:
alexsparkes
2024-11-06 21:51:55 +00:00
parent 2e2c8b41da
commit b3be17c11e
9 changed files with 210 additions and 98 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -59,18 +59,18 @@ function Radio(props) {
key={option.name}
label={option.name}
value={option.value}
className="data-[checked]:bg-white/10 group relative flex cursor-pointer rounded-lg bg-white/5 data-[checked]:hover:bg-neutral-700 hover:bg-neutral-700 py-4 px-5 text-white shadow-md transition focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white"
className="data-[checked]:bg-white/10 group relative flex cursor-pointer rounded-lg bg-white/5 data-[checked]:hover:bg-neutral-700 hover:bg-neutral-700 py-4 px-5 dark:text-white text-black shadow-md transition focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white"
>
<div className="flex w-full items-center justify-between">
<div className="text-sm/6">
<p className="font-semibold text-white capitalize">{option.name}</p>
<div className="flex gap-2 text-white/50">
<p className="font-semibold capitalize">{option.name}</p>
<div className="flex gap-2 dark:text-white/50">
<div>{option.subname}</div>
<div aria-hidden="true">&middot;</div>
<div>10%</div>
</div>
</div>
<MdCheckCircle className="size-6 fill-white opacity-0 transition group-data-[checked]:opacity-100" />
<MdCheckCircle className="size-6 dark:fill-white fill-black opacity-0 transition group-data-[checked]:opacity-100" />
</div>
</PureRadio>
))}

View File

@@ -1,20 +1,25 @@
import variables from 'config/variables';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { Field, Label, Textarea, Input } from '@headlessui/react';
import { MdRefresh } from 'react-icons/md';
import clsx from 'clsx';
import EventBus from 'utils/eventbus';
function Text(props) {
const [value, setValue] = useState(localStorage.getItem(props.name) || '');
useEffect(() => {
if (props.code) {
// import('prismjs/themes/prism.css');
const Prism = require('prismjs');
Prism.highlightAll();
}
}, [value, props.code]);
const handleChange = (e) => {
let { value } = e.target;
// Alex wanted font to work with montserrat and Montserrat, so I made it work
if (props.upperCaseFirst === true) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
@@ -62,6 +67,10 @@ function Text(props) {
rows={4}
/>
</>
) : props.code ? (
<pre className="mt-3 block w-full rounded-lg bg-white/5 py-1.5 px-3 text-sm/6 text-white border border-[#484848]">
<code className="language-css">{value}</code>
</pre>
) : (
<>
<Input

View File

@@ -1,27 +1,26 @@
import variables from 'config/variables';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { MdStar, MdStarBorder } from 'react-icons/md';
import defaults from '../options/default';
function Favourite({ credit, offline, pun, tooltipText }) {
const [favourited, setFavourited] = useState(
localStorage.getItem('favourite') ? (
<MdStar onClick={() => favourite()} className="topicons" />
) : (
<MdStarBorder onClick={() => favourite()} className="topicons" />
),
);
const [favourited, setFavourited] = useState(false);
const buttons = {
favourited: <MdStar onClick={() => favourite()} className="topicons" />,
unfavourited: <MdStarBorder onClick={() => favourite()} className="topicons" />,
};
useEffect(() => {
const isFavourited = localStorage.getItem('favourite') !== null;
setFavourited(isFavourited);
tooltipText(
isFavourited
? variables.getMessage('widgets.quote.unfavourite')
: variables.getMessage('widgets.quote.favourite'),
);
}, [tooltipText]);
async function favourite() {
const favourite = async () => {
if (localStorage.getItem('favourite')) {
localStorage.removeItem('favourite');
setFavourited(buttons.unfavourited);
setFavourited(false);
tooltipText(variables.getMessage('widgets.quote.favourite'));
variables.stats.postEvent('feature', 'background', 'favourite');
} else {
@@ -82,31 +81,30 @@ function Favourite({ credit, offline, pun, tooltipText }) {
pun,
}),
);
setFavourited(buttons.favourited);
tooltipText(variables.getMessage('widgets.quote.unfavourite'));
variables.stats.postEvent('feature', 'background', 'unfavourite');
}
}
return setFavourited(buttons.favourited);
setFavourited(true);
tooltipText(variables.getMessage('widgets.quote.unfavourite'));
variables.stats.postEvent('feature', 'background', 'unfavourite');
return;
}
setFavourited(true);
tooltipText(variables.getMessage('widgets.quote.unfavourite'));
variables.stats.postEvent('feature', 'background', 'unfavourite');
}
}
};
if (localStorage.getItem('backgroundType') === 'colour') {
return null;
}
tooltipText(
localStorage.getItem('favourite')
? variables.getMessage('widgets.quote.unfavourite')
: variables.getMessage('widgets.quote.favourite'),
return favourited ? (
<MdStar onClick={favourite} className="topicons" />
) : (
<MdStarBorder onClick={favourite} className="topicons" />
);
return favourited;
}
export default Favourite;

View File

@@ -1,5 +1,5 @@
import variables from 'config/variables';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { MdEmail, MdContactPage } from 'react-icons/md';
import { FaDiscord } from 'react-icons/fa';
import { SiGithubsponsors, SiOpencollective, SiX } from 'react-icons/si';
@@ -18,15 +18,20 @@ function About() {
);
const [loading, setLoading] = useState(variables.getMessage('modals.main.loading'));
const controller = new AbortController();
const controller = useMemo(() => new AbortController(), []);
useEffect(() => {
return () => {
controller.abort();
};
}, []);
}, [controller]);
const getGitHubData = async () => {
let contributors, sponsors, photographers, curators, versionData;
const getGitHubData = useCallback(async () => {
let contributors = [];
let sponsors = [];
let photographers = [];
let curators = [];
let versionData = [];
try {
versionData = await (
@@ -81,7 +86,7 @@ function About() {
setLoading(variables.getMessage('settings:sections.about.version.error.description'));
}
if (sponsors.length === 0) {
if (!sponsors || sponsors.length === 0) {
sponsors = [{ handle: 'empty' }];
}
@@ -89,7 +94,7 @@ function About() {
return;
}
const newVersion = versionData[0].tag_name;
const newVersion = versionData[0]?.tag_name || '';
let update = variables.getMessage('settings:sections.about.version.no_update');
@@ -108,7 +113,7 @@ function About() {
setPhotographers(photographers);
setCurators(curators);
setLoading(null);
};
}, [controller]);
useEffect(() => {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
@@ -118,11 +123,11 @@ function About() {
}
getGitHubData();
}, []);
}, [getGitHubData]);
return (
<div className="modalInfoPage">
<div className="settingsRow" style={{ justifyContent: 'center' }}>
const Header = () => {
return (
<div className="settingsRow" style={{ justifyContent: 'center', border: 'none' }}>
<div style={{ display: 'flex', flexFlow: 'column', gap: '5px' }}>
<img draggable={false} className="aboutLogo" src={'icons/mue_about.png'} alt="Logo" />
<div className="aboutText">
@@ -179,9 +184,15 @@ function About() {
</span>
</div>
</div>
);
};
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">{variables.getMessage('settings:sections.about.contact_us')}</span>
const ContactBlock = () => {
return (
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-col items-start gap-5">
<span className="text-2xl font-semibold">
{variables.getMessage('settings:sections.about.contact_us')}
</span>
<div className="aboutContact">
<Button
type="linkButton"
@@ -209,9 +220,15 @@ function About() {
/>
</div>
</div>
);
};
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">{variables.getMessage('settings:sections.about.support_mue')}</span>
const SponsorBlock = () => {
return (
<div className="bg-modal-content-light dark:bg-modal-content-dark py-6 px-10 rounded flex flex-col items-start gap-5">
<span className="text-2xl font-semibold">
{variables.getMessage('settings:sections.about.support_mue')}
</span>
<p>{variables.getMessage('settings:sections.about.support_subtitle')}</p>
<div className="aboutContact">
<Button
@@ -234,6 +251,36 @@ function About() {
/>
</div>
</div>
);
};
return (
<div className="modalInfoPage">
<Header />
<div className="grid grid-cols-2 gap-10">
<ContactBlock />
<SponsorBlock />
</div>
<div className="flex flex-col px-10">
<span className="py-10 text-2xl font-semibold">
{variables.getMessage('settings:sections.about.contributors')}
</span>
<p>{loading}</p>
<div className="contributorImages">
{contributors.map(({ login, id }) => (
<Tooltip title={login} key={login}>
<a href={'https://github.com/' + login} target="_blank" rel="noopener noreferrer">
<img
draggable={false}
src={'https://avatars.githubusercontent.com/u/' + id + '?s=128'}
alt={login}
></img>
</a>
</Tooltip>
))}
</div>
</div>
<div
className="settingsRow"
@@ -259,26 +306,6 @@ function About() {
</span>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">
{variables.getMessage('settings:sections.about.contributors')}
</span>
<p>{loading}</p>
<div className="contributorImages">
{contributors.map(({ login, id }) => (
<Tooltip title={login} key={login}>
<a href={'https://github.com/' + login} target="_blank" rel="noopener noreferrer">
<img
draggable={false}
src={'https://avatars.githubusercontent.com/u/' + id + '?s=128'}
alt={login}
></img>
</a>
</Tooltip>
))}
</div>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">{variables.getMessage('settings:sections.about.supporters')}</span>
<p>{loading}</p>

View File

@@ -119,7 +119,7 @@ function AdvancedOptions() {
<span className="text-3xl font-semibold tracking-tight">
{variables.getMessage('settings:sections.advanced.custom_css')}
</span>
<Text name="customcss" textarea={true} category="other" customcss={true} />
<Text name="customcss" textarea={true} category="other" code={true} />
</PreferencesWrapper>
)}
{subSection === '' && (

View File

@@ -1,21 +1,60 @@
import { achievements } from './index';
import statsModule from '../stats';
export async function checkAchievements() {
// First get the events data which contains all user actions
const events = await statsModule.getStats();
// Calculate aggregated stats
const aggregatedStats = {
'new-tab': events.filter((event) => event.type === 'new-tab').length,
'marketplace-install': events.filter((event) => event.type === 'marketplace-install').length,
totalXp: events.reduce((sum, event) => sum + (event.xpGained || 0), 0),
level: statsModule.level || 1,
};
console.log('Checking achievements with aggregated stats:', aggregatedStats);
export function checkAchievements(stats) {
achievements.forEach((achievement) => {
switch (achievement.condition.type) {
case 'tabsOpened':
if (stats['tabs-opened']?.count >= achievement.condition.amount) {
console.log(
`Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['new-tab']}`,
);
if (aggregatedStats['new-tab'] >= achievement.condition.amount) {
achievement.achieved = true;
}
break;
case 'addonInstall':
if (stats.marketplace) {
if (stats.marketplace['install']?.count >= achievement.condition.amount) {
achievement.achieved = true;
}
console.log(
`Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['marketplace-install']}`,
);
if (aggregatedStats['marketplace-install'] >= achievement.condition.amount) {
achievement.achieved = true;
}
break;
case 'reachLevel':
console.log(
`Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.level}`,
);
if (aggregatedStats.level >= achievement.condition.amount) {
achievement.achieved = true;
}
break;
case 'earnXp':
console.log(
`Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.totalXp}`,
);
if (aggregatedStats.totalXp >= achievement.condition.amount) {
achievement.achieved = true;
}
break;
default:
console.warn(`Unknown achievement condition type: ${achievement.condition.type}`);
break;
}
});
@@ -23,25 +62,62 @@ export function checkAchievements(stats) {
return achievements;
}
export function newAchievements(stats) {
// calculate new achievements
export async function newAchievements() {
// Get previously achieved achievements
const oldAchievements = JSON.parse(localStorage.getItem('achievements')) || [];
const checkedAchievements = checkAchievements(stats);
// Check for new achievements
const checkedAchievements = await checkAchievements();
const newAchievements = [];
checkedAchievements.forEach((achievement) => {
if (achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id)) {
newAchievements.push(achievement);
const isNewAchievement =
achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id);
if (isNewAchievement) {
const newAchievement = {
...achievement,
timestamp: new Date().toISOString(),
};
newAchievements.push(newAchievement);
}
});
// add timestamp to new achievements
newAchievements.forEach((achievement) => {
achievement.timestamp = new Date().toISOString();
});
// save new achievements to local storage
localStorage.setItem('achievements', JSON.stringify([...oldAchievements, ...newAchievements]));
// Update stored achievements
if (newAchievements.length > 0) {
const updatedAchievements = [...oldAchievements, ...newAchievements];
localStorage.setItem('achievements', JSON.stringify(updatedAchievements));
}
return newAchievements;
}
// Helper function to get achievement progress
export async function getAchievementProgress(achievementId) {
const achievement = achievements.find((a) => a.id === achievementId);
if (!achievement) return null;
const events = await statsModule.getStats();
let current = 0;
switch (achievement.condition.type) {
case 'tabsOpened':
current = events.filter((event) => event.type === 'new-tab').length;
break;
case 'addonInstall':
current = events.filter((event) => event.type === 'marketplace-install').length;
break;
case 'reachLevel':
current = statsModule.level || 1;
break;
case 'earnXp':
current = events.reduce((sum, event) => sum + (event.xpGained || 0), 0);
break;
}
return {
current,
required: achievement.condition.amount,
percentage: Math.min(100, Math.floor((current / achievement.condition.amount) * 100)),
};
}

View File

@@ -5,7 +5,7 @@ import variables from 'config/variables';
export default class Stats {
static async achievementTrigger(stats) {
const newAchievement = newAchievements(stats);
const newAchievement = await newAchievements(stats);
newAchievement.forEach((achievement) => {
if (achievement) {
const { name } = getLocalisedAchievementData(achievement.id);
@@ -54,7 +54,7 @@ export default class Stats {
return Math.round(baseXp * (1 + streak * 0.1));
}
static async addEvent(eventLog, event) {
static async addEvent(event) {
await addEvent(event);
}
@@ -118,12 +118,14 @@ export default class Stats {
console.log(`Event Action: ${action}`);
const xpGained = this.calculateXpForEvent(type, statsData.streak.current);
await this.addEvent(eventLog, { type, name, action, timestamp, xpGained });
await this.addEvent({ type, name, action, timestamp, xpGained });
this.updateStatsData(statsData, xpGained);
this.calculateStreak(statsData, timestamp);
console.log(`Updated Stats Data: ${JSON.stringify(statsData)}`);
localStorage.setItem('statsData', JSON.stringify(statsData));
this.achievementTrigger(statsData);
await this.achievementTrigger(statsData);
}
static async getStats(type, name, action, startDate, endDate) {

View File

@@ -24,10 +24,10 @@ const Stats = () => {
);
const [clearmodal, setClearmodal] = useState(false);
const updateAchievements = useCallback(() => {
const achieved = checkAchievements(stats);
const updateAchievements = useCallback(async () => {
const achieved = await checkAchievements();
setAchievements(achieved);
}, [stats]);
}, []);
useEffect(() => {
updateAchievements();