mirror of
https://github.com/mue/mue.git
synced 2026-07-16 05:23:49 +02:00
fix(achievments): Make achievements work with new IndexedDB
- currently has extremely verbose logs in console, to be removed later
This commit is contained in:
@@ -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">·</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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 === '' && (
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user