Compare commits

...

1 Commits

Author SHA1 Message Date
alexsparkes
05cc274b27 Refactor: welcome modal and initial background loader 2026-01-30 23:39:45 +00:00
23 changed files with 427 additions and 772 deletions

View File

@@ -73,9 +73,7 @@ export async function fetchAPIImageData(excludedPun = null) {
* Gets background data based on current configuration
*/
export async function getBackgroundData() {
const isOffline =
localStorage.getItem('offlineMode') === 'true' ||
localStorage.getItem('showWelcome') === 'true';
const isOffline = localStorage.getItem('offlineMode') === 'true';
// Handle favourited background
const fav = parseJSON('favourite');

View File

@@ -8,9 +8,7 @@ import { getBackgroundFilterStyle, getBackgroundOverlayStyle } from '../api/back
export function useBackgroundEvents(backgroundData, refreshBackground) {
useEffect(() => {
const handleEvent = (event) => {
if (event === 'welcomeLanguage') {
localStorage.setItem('welcomeImage', JSON.stringify(backgroundData));
} else if (event === 'background') {
if (event === 'background') {
handleVisibilityToggle();
} else if (['marketplacebackgrounduninstall', 'backgroundwelcome', 'backgroundrefresh'].includes(event)) {
refreshBackground();

View File

@@ -12,16 +12,6 @@ export function useBackgroundLoader(updateBackground, resetBackground) {
isLoadingRef.current = true;
try {
// Check for welcome tab first
const welcomeTab = localStorage.getItem('welcomeTab');
if (welcomeTab) {
const welcomeImage = localStorage.getItem('welcomeImage');
if (welcomeImage) {
updateBackground(JSON.parse(welcomeImage));
return;
}
}
const data = await getBackgroundData();
if (data) {
updateBackground(data);

View File

@@ -124,18 +124,19 @@ const Modals = () => {
const closeWelcome = async () => {
localStorage.setItem('showWelcome', false);
localStorage.setItem('justCompletedWelcome', 'true');
setWelcomeModal(false);
await tryInstallDefaultPack();
EventBus.emit('refresh', 'widgetsWelcomeDone');
EventBus.emit('refresh', 'widgets');
EventBus.emit('refresh', 'backgroundwelcome');
};
const previewWelcome = () => {
localStorage.setItem('showWelcome', false);
localStorage.setItem('welcomePreview', true);
localStorage.setItem('justCompletedWelcome', 'true');
setWelcomeModal(false);
setPreview(true);
EventBus.emit('refresh', 'widgetsWelcome');
@@ -181,7 +182,7 @@ const Modals = () => {
onRequestClose={() => closeWelcome()}
isOpen={welcomeModal}
className="Modal welcomemodal mainModal"
overlayClassName="Overlay mainModal"
overlayClassName="Overlay welcomeOverlay"
shouldCloseOnOverlayClick={false}
ariaHideApp={false}
>

View File

@@ -19,8 +19,9 @@ const Weather = lazy(() => import('../../weather/Weather'));
const Widgets = () => {
const online = localStorage.getItem('offlineMode') === 'false';
const [order, setOrder] = useState(JSON.parse(localStorage.getItem('order')));
const [order, setOrder] = useState(JSON.parse(localStorage.getItem('order')) || []);
const [welcome, setWelcome] = useState(localStorage.getItem('showWelcome'));
const [fadeIn, setFadeIn] = useState(false);
const enabled = (key) => {
return localStorage.getItem(key) === 'true';
@@ -43,7 +44,7 @@ const Widgets = () => {
const handleRefresh = (data) => {
switch (data) {
case 'widgets':
return setOrder(JSON.parse(localStorage.getItem('order')));
return setOrder(JSON.parse(localStorage.getItem('order')) || []);
case 'widgetsWelcome':
setWelcome(localStorage.getItem('showWelcome'));
localStorage.setItem('showWelcome', true);
@@ -54,6 +55,15 @@ const Widgets = () => {
case 'widgetsWelcomeDone':
setWelcome(localStorage.getItem('showWelcome'));
window.onbeforeunload = null;
// Check if user just completed welcome
if (localStorage.getItem('justCompletedWelcome') === 'true') {
setFadeIn(true);
// Clear the flag after animations complete
setTimeout(() => {
localStorage.removeItem('justCompletedWelcome');
}, 2000);
}
break;
default:
break;
@@ -66,11 +76,15 @@ const Widgets = () => {
};
}, []);
// don't show when welcome is there
return welcome !== 'false' ? (
<WidgetsLayout />
) : (
<WidgetsLayout>
// Determine class based on state
const getClassName = () => {
if (welcome !== 'false') return 'behind-welcome';
if (fadeIn) return 'fade-in-widgets';
return '';
};
return (
<WidgetsLayout className={getClassName()}>
<Suspense fallback={<></>}>
{enabled('searchBar') && <Search />}
{order.map((element, key) => (

View File

@@ -1,141 +1,16 @@
// Importing necessary libraries and components
import { useState, useEffect } from 'react';
import variables from 'config/variables';
import { MdArrowBackIosNew, MdArrowForwardIos, MdOutlinePreview } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import { ProgressBar, AsideImage } from './components/Elements';
import { Button } from 'components/Elements';
import { Wrapper, Panel } from './components/Layout';
import { SimpleWelcome } from './components/Sections';
import './welcome.scss';
import {
Intro,
ChooseLanguage,
ImportSettings,
ThemeSelection,
StyleSelection,
PrivacyOptions,
Final,
} from './components/Sections';
// WelcomeModal component
function WelcomeModal({ modalClose, modalSkip }) {
// State variables
const [currentTab, setCurrentTab] = useState(0);
const [buttonText, setButtonText] = useState(variables.getMessage('modals.welcome.buttons.next'));
const [importedSettings, setImportedSettings] = useState([]);
const finalTab = 6;
// useEffect hook to handle tab changes
useEffect(() => {
// Get the current welcome tab from local storage
const welcomeTab = localStorage.getItem('welcomeTab');
if (welcomeTab) {
const tab = Number(welcomeTab);
setCurrentTab(tab);
setButtonText(
tab !== finalTab + 1
? variables.getMessage('modals.welcome.buttons.next')
: variables.getMessage('modals.welcome.buttons.finish'),
);
}
}, [finalTab]);
// Function to update the current tab and button text
const updateTabAndButtonText = (newTab) => {
setCurrentTab(newTab);
setButtonText(
newTab !== finalTab
? variables.getMessage('modals.welcome.buttons.next')
: variables.getMessage('modals.welcome.buttons.finish'),
);
localStorage.setItem('bgtransition', true);
localStorage.removeItem('welcomeTab');
};
// Functions to navigate to the previous and next tabs
const prevTab = () => {
updateTabAndButtonText(currentTab - 1);
};
const nextTab = () => {
if (buttonText === variables.getMessage('modals.welcome.buttons.finish')) {
modalClose();
return;
}
updateTabAndButtonText(currentTab + 1);
};
// Function to switch to a specific tab
const switchToTab = (tab) => {
updateTabAndButtonText(tab);
};
// Navigation component
const Navigation = () => {
return (
<div className="welcomeButtons">
{currentTab !== 0 ? (
<Button
type="settings"
onClick={() => prevTab()}
icon={<MdArrowBackIosNew />}
label={variables.getMessage('modals.welcome.buttons.previous')}
/>
) : (
<Button
type="settings"
onClick={() => modalSkip()}
icon={<MdOutlinePreview />}
label={variables.getMessage('modals.welcome.buttons.preview')}
/>
)}
<Button
type="settings"
onClick={() => nextTab()}
icon={<MdArrowForwardIos />}
label={buttonText}
iconPlacement={'right'}
/>
</div>
);
};
// Mapping of tab numbers to components
const tabComponents = {
0: <Intro />,
1: <ChooseLanguage />,
2: <ImportSettings setImportedSettings={setImportedSettings} switchTab={switchToTab} />,
3: <ThemeSelection />,
4: <StyleSelection />,
5: <PrivacyOptions />,
6: (
<Final currentTab={currentTab} switchTab={switchToTab} importedSettings={importedSettings} />
),
};
// Current tab component
const CurrentTab = tabComponents[currentTab] || <Intro />;
// Render the WelcomeModal component
// Render the simplified welcome component
return (
<Wrapper>
<Panel type="aside">
<AsideImage currentTab={currentTab} />
<ProgressBar numberOfTabs={finalTab + 1} currentTab={currentTab} switchTab={switchToTab} />
</Panel>
<Panel type="content">
{CurrentTab}
<Navigation
currentTab={currentTab}
changeTab={switchToTab}
buttonText={buttonText}
modalSkip={modalSkip}
/>
<Panel type="content" className="simpleWelcome">
<SimpleWelcome modalClose={modalClose} modalSkip={modalSkip} />
</Panel>
</Wrapper>
);

View File

@@ -1,31 +0,0 @@
const images = [
'/src/assets/icons/undraw_celebration.svg',
'/src/assets/icons/undraw_around_the_world_modified.svg',
'/src/assets/icons/undraw_add_files_modified.svg',
'/src/assets/icons/undraw_dark_mode.svg',
'/src/assets/icons/undraw_making_art.svg',
'/src/assets/icons/undraw_private_data_modified.svg',
'/src/assets/icons/undraw_upgrade_modified.svg',
];
function AsideImage({ currentTab }) {
const altTexts = [
'Celebration icon',
'Around the world icon',
'Add files icon',
'Dark mode icon',
'Making art icon',
'Private data icon',
'Upgrade icon',
];
return (
<img
className="showcaseimg"
alt={altTexts[currentTab]}
draggable={false}
src={images[currentTab]}
/>
);
}
export { AsideImage as default, AsideImage };

View File

@@ -1 +0,0 @@
export * from './AsideImage';

View File

@@ -1,33 +0,0 @@
import { memo } from 'react';
const Step = memo(({ isActive, index, onClick }) => {
const className = isActive ? 'step active' : 'step';
return (
<div className={className} onClick={onClick}>
<span>{index + 1}</span>
</div>
);
});
Step.displayName = 'Step';
function ProgressBar({ numberOfTabs, currentTab, switchTab }) {
return (
<div className="progressbar">
{Array.from({ length: numberOfTabs }, (_, index) => (
<Step
key={index}
isActive={index === currentTab}
index={index}
onClick={() => switchTab(index)}
/>
))}
</div>
);
}
const MemoizedProgressBar = memo(ProgressBar);
export default MemoizedProgressBar;
export { MemoizedProgressBar as ProgressBar };

View File

@@ -1 +0,0 @@
export * from './ProgressBar';

View File

@@ -1,2 +0,0 @@
export * from './ProgressBar';
export * from './AsideImage';

View File

@@ -1,124 +0,0 @@
import { useState, useMemo } from 'react';
import { MdOutlineOpenInNew } from 'react-icons/md';
import languages from '@/i18n/languages.json';
import translationPercentages from '@/i18n/translationPercentages.json';
import { useT, useTranslation } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { Radio, SearchInput } from 'components/Form/Settings';
import { Header, Content } from '../Layout';
function ChooseLanguage() {
const t = useT();
const { language: currentLanguage, changeLanguage } = useTranslation();
const title = t('modals.welcome.sections.language.title');
const description = t('modals.welcome.sections.language.description');
const [searchQuery, setSearchQuery] = useState('');
const languageOptions = useMemo(() => {
const currentLanguageISO = currentLanguage.replace('_', '-');
const displayNames = new Intl.DisplayNames([currentLanguageISO], { type: 'language' });
const mappedLanguages = languages.map((lang) => {
const nativeName = lang.name;
const isoCode = lang.value.replace('_', '-');
const percentage = translationPercentages[lang.value]?.percent || 0;
let translatedName;
try {
translatedName = displayNames.of(isoCode);
if (translatedName) {
translatedName = translatedName.split(' (')[0];
}
} catch {
translatedName = nativeName;
}
const displayName =
!translatedName || translatedName === nativeName ? (
<>
{nativeName} <span style={{ color: '#999', fontSize: '0.85em' }}>({percentage}%)</span>
</>
) : (
<>
{nativeName}{' '}
<span style={{ color: '#999', fontSize: '0.85em' }}>
({translatedName} {percentage}%)
</span>
</>
);
return {
name: displayName,
value: lang.value,
nativeName,
percentage,
searchText: `${nativeName} ${translatedName || ''}`.toLowerCase(),
};
});
// Sort alphabetically by native name
return mappedLanguages.sort((a, b) => a.nativeName.localeCompare(b.nativeName));
}, [currentLanguage]);
// Filter languages based on search query
const filteredLanguages = useMemo(() => {
if (!searchQuery.trim()) return languageOptions;
const query = searchQuery.toLowerCase();
return languageOptions.filter((lang) => lang.searchText.includes(query));
}, [languageOptions, searchQuery]);
// Detect system language
const systemLanguage = useMemo(() => {
const browserLang = navigator.language.replace('-', '_');
return (
languages.find((l) => l.value === browserLang) ||
languages.find((l) => l.value.startsWith(browserLang.split('_')[0]))
);
}, []);
return (
<Content>
<Header
title={title}
subtitle={
<>
{description}{' '}
<a
href={variables.constants.TRANSLATIONS_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.2em' }}
>
GitHub <MdOutlineOpenInNew />
</a>
</>
}
/>
{systemLanguage && systemLanguage.value !== currentLanguage && (
<button
className="uploadbg"
onClick={() => changeLanguage(systemLanguage.value)}
style={{ marginBottom: 12 }}
>
{t('modals.main.settings.sections.language.use_system')} ({systemLanguage.name})
</button>
)}
<div style={{ marginBottom: 16 }}>
<SearchInput
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
fullWidth
/>
</div>
<div className="languageSettings">
<Radio name="language" options={filteredLanguages} category="welcomeLanguage" />
</div>
</Content>
);
}
export { ChooseLanguage as default, ChooseLanguage };

View File

@@ -1,43 +0,0 @@
import variables from 'config/variables';
import languages from '@/i18n/languages.json';
import { Header, Content } from '../Layout';
function Final(props) {
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.final.title')}
subtitle={variables.getMessage('modals.welcome.sections.final.description')}
/>
<span className="title">{variables.getMessage('modals.welcome.sections.final.changes')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.final.changes_description')}
</span>
<div className="themesToggleArea themesToggleAreaWelcome">
<div className="toggle" onClick={() => props.switchTab(1)}>
<span>
{variables.getMessage('modals.main.settings.sections.language.title')}:{' '}
{languages.find((i) => i.value === localStorage.getItem('language')).name}
</span>
</div>
<div className="toggle" onClick={() => props.switchTab(3)}>
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.title')}:{' '}
{variables.getMessage(
'modals.main.settings.sections.appearance.theme.' + localStorage.getItem('theme'),
)}
</span>
</div>
{props.importedSettings.length !== 0 && (
<div className="toggle" onClick={() => props.switchTab(2)}>
{variables.getMessage('modals.welcome.sections.final.imported', {
amount: props.importedSettings.length,
})}{' '}
</div>
)}
</div>
</Content>
);
}
export { Final as default, Final };

View File

@@ -1,66 +0,0 @@
import variables from 'config/variables';
import { FileUpload } from 'components/Form/Settings';
import { MdCloudUpload } from 'react-icons/md';
import { importSettings as importSettingsFunction } from 'utils/settings';
import { Header, Content } from '../Layout';
import default_settings from 'utils/data/default_settings.json';
function ImportSettings(props) {
const importSettings = (e) => {
importSettingsFunction(e, true);
const settings = [];
const data = JSON.parse(e);
Object.keys(data).forEach((setting) => {
// language and theme already shown, the others are only used internally
if (
setting === 'language' ||
setting === 'theme' ||
setting === 'firstRun' ||
setting === 'showWelcome' ||
setting === 'showReminder'
) {
return;
}
const defaultSetting = default_settings.find((i) => i.name === setting);
if (defaultSetting !== undefined) {
if (data[setting] === String(defaultSetting.value)) {
return;
}
}
settings.push({
name: setting,
value: data[setting],
});
});
props.setImportedSettings(settings);
props.switchTab(6);
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.settings.title')}
subtitle={variables.getMessage('modals.welcome.sections.settings.description')}
/>
<button className="upload" onClick={() => document.getElementById('file-input').click()}>
<MdCloudUpload />
<span>{variables.getMessage('modals.main.settings.buttons.import')}</span>
</button>
<FileUpload
id="file-input"
accept="application/json"
type="settings"
loadFunction={(e) => importSettings(e)}
/>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.settings.tip')}
</span>
</Content>
);
}
export { ImportSettings as default, ImportSettings };

View File

@@ -1,91 +0,0 @@
import variables from 'config/variables';
import { useState, useEffect, useCallback } from 'react';
import { Header, Content } from '../Layout';
import { MdOutlineWavingHand, MdOpenInNew } from 'react-icons/md';
import { FaDiscord, FaGithub } from 'react-icons/fa';
const DISCORD_LINK = 'https://discord.gg/' + variables.constants.DISCORD_SERVER;
const GITHUB_LINK =
'https://github.com/' + variables.constants.ORG_NAME + '/' + variables.constants.REPO_NAME;
function WelcomeNotice({ config }) {
const { icon: Icon, title, subtitle, link } = config;
return (
<div className="welcomeNotice">
<div className="icon">
<Icon />
</div>
<div className="text">
<span className="title">{title}</span>
<span className="subtitle">{subtitle}</span>
</div>
{link && (
<a href={link} target="_blank" rel="noopener noreferrer">
<MdOpenInNew />
{variables.getMessage('modals.welcome.sections.intro.notices.github_open')}
</a>
)}
</div>
);
}
function Intro() {
const [welcomeImage, setWelcomeImage] = useState(0);
const updateWelcomeImage = useCallback(() => {
setWelcomeImage((prevWelcomeImage) => (prevWelcomeImage < 3 ? prevWelcomeImage + 1 : 0));
}, []);
const ShareYourMue = (
<div className="examples">
<img
src={`/src/assets/welcome-images/example${welcomeImage + 1}.webp`}
alt="Example Mue setup"
draggable={false}
/>
<span className="shareYourMue">#shareyourmue</span>
</div>
);
useEffect(() => {
const timer = setInterval(updateWelcomeImage, 3000);
return () => clearInterval(timer);
}, [updateWelcomeImage]);
return (
<Content>
<Header title={variables.getMessage('modals.welcome.sections.intro.title')} />
{ShareYourMue}
<WelcomeNotice
config={{
icon: MdOutlineWavingHand,
title: variables.getMessage('modals.welcome.sections.intro.title'),
subtitle: variables.getMessage('modals.welcome.sections.intro.description'),
}}
/>
<WelcomeNotice
config={{
icon: FaDiscord,
title: variables.getMessage('modals.welcome.sections.intro.notices.discord_title'),
subtitle: variables.getMessage(
'modals.welcome.sections.intro.notices.discord_description',
),
link: DISCORD_LINK,
}}
/>
<WelcomeNotice
config={{
icon: FaGithub,
title: variables.getMessage('modals.welcome.sections.intro.notices.github_title'),
subtitle: variables.getMessage(
'modals.welcome.sections.intro.notices.github_description',
),
link: GITHUB_LINK,
}}
/>
</Content>
);
}
export { Intro as default, Intro };

View File

@@ -1,63 +0,0 @@
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md';
import { Checkbox } from 'components/Form/Settings';
import { Header, Content } from '../Layout';
function OfflineMode() {
return (
<>
<Checkbox
name="offlineMode"
text={variables.getMessage('modals.main.settings.sections.advanced.offline_mode')}
element=".other"
/>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.offline_mode_description')}
</span>
</>
);
}
function Links() {
return (
<>
<span className="title">
{variables.getMessage('modals.welcome.sections.privacy.links.title')}
</span>
<a
className="link"
href={variables.constants.PRIVACY_URL}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.privacy_policy')}
<MdOutlineOpenInNew />
</a>
<a
className="link"
href={`https://github.com/${variables.constants.ORG_NAME}`}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.source_code')}
<MdOutlineOpenInNew />
</a>
</>
);
}
function PrivacyOptions() {
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.privacy.title')}
subtitle={variables.getMessage('modals.welcome.sections.privacy.description')}
/>
<OfflineMode />
<Links />
</Content>
);
}
export { PrivacyOptions as default, PrivacyOptions };

View File

@@ -0,0 +1,104 @@
import variables from 'config/variables';
import { useState } from 'react';
import { Button } from 'components/Elements';
import EventBus from 'utils/eventbus';
import { FaDiscord, FaGithub, FaTwitter, FaEnvelope } from 'react-icons/fa';
function SimpleWelcome({ modalClose, modalSkip }) {
const [name, setName] = useState('');
const handleNameChange = (e) => {
const newName = e.target.value;
setName(newName);
// Live update localStorage and trigger greeting refresh
if (newName.trim()) {
localStorage.setItem('greetingName', newName.trim());
} else {
localStorage.removeItem('greetingName');
}
EventBus.emit('refresh', 'greeting');
};
const handleContinue = () => {
// Name is already saved via live updates
modalClose();
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleContinue();
}
};
return (
<div className="simpleWelcomeContainer">
<div className="welcomeContent">
<div className="welcomeLogoSection">
<img
src="src/assets/icons/mue_about.png"
alt="Mue"
className="mueLogo"
draggable={false}
/>
{/* <h1 className="welcomeTitle">Welcome</h1> */}
</div>
<div className="welcomeNameSection">
<label className="nameLabel">
{variables.getMessage('modals.welcome.sections.intro.name_label')}
</label>
<input
type="text"
className="nameInput"
placeholder={variables.getMessage('modals.welcome.sections.intro.name_placeholder')}
value={name}
onChange={handleNameChange}
onKeyPress={handleKeyPress}
autoFocus
/>
</div>
<div className="welcomeActions">
<button className="skipButton" onClick={modalSkip}>
{variables.getMessage('modals.welcome.buttons.skip')}
</button>
<Button
type="settings"
label={variables.getMessage('modals.welcome.buttons.continue')}
onClick={handleContinue}
/>
</div>
</div>
<div className="welcomeCopyright">
<div className="icons">
<a
href={`https://discord.com/${variables.constants.DISCORD_HANDLE}`}
target="_blank"
rel="noopener noreferrer"
>
<FaDiscord />
</a>
<a
href={`https://github.com/${variables.constants.GITHUB_HANDLE}`}
target="_blank"
rel="noopener noreferrer"
>
<FaGithub />
</a>
<a
href={`https://twitter.com/${variables.constants.TWITTER_HANDLE}`}
target="_blank"
rel="noopener noreferrer"
>
<FaTwitter />
</a>
</div>
<span>© {variables.getMessage('branding.author')}</span>
</div>
</div>
);
}
export { SimpleWelcome as default, SimpleWelcome };

View File

@@ -1,53 +0,0 @@
import variables from 'config/variables';
import { MdArchive, MdOutlineWhatshot } from 'react-icons/md';
import { useState } from 'react';
import { Header, Content } from '../Layout';
const STYLES = {
NEW: 'new',
LEGACY: 'legacy',
};
const StyleSelection = () => {
const widgetStyle = localStorage.getItem('widgetStyle') || STYLES.NEW;
const [style, setStyle] = useState(widgetStyle);
const changeStyle = (type) => {
setStyle(type);
localStorage.setItem('widgetStyle', type);
};
const styleMapping = {
[STYLES.LEGACY]: {
className: style === STYLES.LEGACY ? 'toggle legacyStyle active' : 'toggle legacyStyle',
icon: <MdArchive />,
text: variables.getMessage('modals.welcome.sections.style.legacy'),
},
[STYLES.NEW]: {
className: style === STYLES.NEW ? 'toggle newStyle active' : 'toggle newStyle',
icon: <MdOutlineWhatshot />,
text: variables.getMessage('modals.welcome.sections.style.modern'),
},
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.style.title')}
subtitle={variables.getMessage('modals.welcome.sections.style.description')}
/>
<div className="themesToggleArea">
<div className="options">
{Object.entries(styleMapping).map(([type, { className, icon, text }]) => (
<div className={className} onClick={() => changeStyle(type)} key={type}>
{icon}
<span>{text}</span>
</div>
))}
</div>
</div>
</Content>
);
};
export { StyleSelection as default, StyleSelection };

View File

@@ -1,72 +0,0 @@
import variables from 'config/variables';
import { useState } from 'react';
import { MdAutoAwesome, MdLightMode, MdDarkMode } from 'react-icons/md';
import { loadSettings } from 'utils/settings';
import { Header, Content } from '../Layout';
const THEMES = {
AUTO: 'auto',
LIGHT: 'light',
DARK: 'dark',
};
function ThemeSelection() {
const currentTheme = localStorage.getItem('theme') || THEMES.AUTO;
const [theme, setTheme] = useState(currentTheme);
const changeTheme = (type) => {
setTheme(type);
localStorage.setItem('theme', type);
loadSettings(true);
};
const themeMapping = {
[THEMES.AUTO]: {
className: theme === THEMES.AUTO ? 'toggle auto active' : 'toggle auto',
icon: <MdAutoAwesome />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.auto'),
},
[THEMES.LIGHT]: {
className: theme === THEMES.LIGHT ? 'toggle lightTheme active' : 'toggle lightTheme',
icon: <MdLightMode />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.light'),
},
[THEMES.DARK]: {
className: theme === THEMES.DARK ? 'toggle darkTheme active' : 'toggle darkTheme',
icon: <MdDarkMode />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.dark'),
},
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.theme.title')}
subtitle={variables.getMessage('modals.welcome.sections.theme.description')}
/>
<div className="themesToggleArea">
<div
className={themeMapping[THEMES.AUTO].className}
onClick={() => changeTheme(THEMES.AUTO)}
>
{themeMapping[THEMES.AUTO].icon}
<span>{themeMapping[THEMES.AUTO].text}</span>
</div>
<div className="options">
{Object.entries(themeMapping)
.filter(([type]) => type !== THEMES.AUTO)
.map(([type, { className, icon, text }]) => (
<div className={className} onClick={() => changeTheme(type)} key={type}>
{icon}
<span>{text}</span>
</div>
))}
</div>
</div>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">{variables.getMessage('modals.welcome.sections.theme.tip')}</span>
</Content>
);
}
export { ThemeSelection as default, ThemeSelection };

View File

@@ -1,7 +1 @@
export * from './Intro';
export * from './ChooseLanguage';
export * from './ImportSettings';
export * from './ThemeSelection';
export * from './StyleSelection';
export * from './PrivacyOptions';
export * from './Final';
export * from './SimpleWelcome';

View File

@@ -3,7 +3,34 @@
.welcomemodal {
@include themed {
background-color: t($modal-background);
background-color: t($modal);
}
// Disable opening animation - appear instantly
&.ReactModal__Content {
transform: scale(1);
opacity: 1;
}
// Smooth fade out on close
&.ReactModal__Content--before-close {
opacity: 0;
transform: scale(1);
transition: opacity 0.3s ease-out;
}
}
.welcomeOverlay {
background-color: rgba(0, 0, 0, 0.9) !important;
// Instant appear, fade out on close
&.ReactModal__Overlay {
opacity: 1;
transition: opacity 0.3s ease-out;
}
&.ReactModal__Overlay--before-close {
opacity: 0;
}
}
@@ -11,7 +38,6 @@
@include themed {
background-color: t($modal-background);
}
.MuiFormControlLabel-root {
margin-right: 0;
}
@@ -27,10 +53,8 @@
height: 80vh;
width: clamp(60vw, 1200px, 90vw);
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(1, 1fr);
grid-gap: 0;
display: flex;
flex-direction: column;
section.aside {
display: flex;
@@ -48,6 +72,14 @@
justify-content: space-between;
overflow-y: auto;
&.simpleWelcome {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
max-width: none;
}
.content {
display: flex;
flex-flow: column;
@@ -57,35 +89,208 @@
}
}
.progressbar {
position: fixed;
bottom: 50px;
text-align: center;
display: inline;
overflow: hidden;
white-space: nowrap;
font-size: 18px;
.mueLogo {
width: 50px;
height: auto;
position: absolute;
top: 50px;
margin: 0 auto;
}
.step {
display: inline-block;
border-bottom: 2px solid grey;
padding: 10px 20px;
margin: 5px;
transition: 0.2s ease-in-out;
cursor: pointer;
border-radius: 10px 10px 0 0;
.simpleWelcomeContainer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
&:hover {
background: #dd4038;
border-radius: 10px;
border-bottom: 2px solid #dd4038;
@include themed {
background-color: t($modal);
}
.welcomeContent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.welcomeLogoSection {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
margin-bottom: 3rem;
.welcomeTitle {
font-size: 2.5rem;
font-weight: 300;
margin: 0;
letter-spacing: -0.02em;
@include themed {
color: t($color);
}
}
}
.active {
background: #d21a11;
border-bottom: 2px solid #d21a11;
border-radius: 10px;
.welcomeNameSection {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 400px;
margin-bottom: 3rem;
.nameLabel {
font-size: 1rem;
font-weight: 400;
opacity: 0.7;
@include themed {
color: t($color);
}
}
.nameInput {
width: 100%;
padding: 1rem 1.5rem;
font-size: 1.1rem;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
transition: all 0.2s ease;
@include themed {
color: t($color);
}
&::placeholder {
opacity: 0.4;
}
&:focus {
outline: none;
border-color: rgba(210, 26, 17, 0.5);
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 0 0 3px rgba(210, 26, 17, 0.1);
}
}
}
.welcomeActions {
display: flex;
gap: 2rem;
align-items: center;
.skipButton {
background: none;
border: none;
font-size: 1rem;
opacity: 0.6;
cursor: pointer;
padding: 0.75rem 1rem;
transition: opacity 0.2s ease;
@include themed {
color: t($color);
}
&:hover {
opacity: 1;
}
}
button:not(.skipButton) {
min-width: 120px;
padding: 0.75rem 2rem;
font-size: 1rem;
border-radius: 10px;
transition: all 0.2s ease;
}
}
.welcomeCopyright {
position: absolute;
bottom: 2rem;
font-size: 1rem;
opacity: 0.4;
font-weight: 300;
display: flex;
flex-flow: column;
align-items: center;
gap: 0.25rem;
.icons {
display: flex;
flex-flow: row;
gap: 0.5rem;
}
a {
text-decoration: none;
color: var(--modal-text);
font-weight: 500;
transition: all 0.2s ease;
&:hover {
scale: 1.1;
@include themed() {
color: t($link);
}
}
}
@include themed {
color: t($color);
}
}
}
.welcomeNameInput {
margin: 2rem 0;
text-align: center;
label {
display: block;
font-size: 1.1rem;
margin-bottom: 0.5rem;
font-weight: 500;
}
input {
width: 100%;
max-width: 400px;
padding: 0.75rem;
font-size: 1rem;
border: none;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
background: transparent;
color: white;
text-align: center;
transition: border-color 0.2s;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
&:focus {
outline: none;
border-bottom-color: rgba(255, 255, 255, 0.7);
}
}
.helperText {
display: block;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
margin-top: 0.5rem;
}
}

View File

@@ -657,7 +657,11 @@
"sections": {
"intro": {
"title": "Welcome to Mue Tab",
"subtitle": "Your new tab, your way",
"description": "Thank you for installing Mue, we hope you enjoy your time with our extension.",
"name_label": "So, what should we call you?",
"name_placeholder": "Name",
"name_helper": "We'll use this in your tab",
"notices": {
"discord_title": "Join our Discord",
"discord_description": "Talk with the Mue community and developers",
@@ -710,7 +714,9 @@
"preview": "Preview",
"previous": "Previous",
"close": "Close",
"finish": "Finish"
"finish": "Finish",
"continue": "Continue",
"skip": "Skip"
},
"preview": {
"description": "You are currently in preview mode. Settings will be reset on closing this tab.",

View File

@@ -34,6 +34,56 @@ body {
align-items: center;
gap: 20px;
animation: fadeIn 1s;
&.widgets-hidden {
opacity: 0;
pointer-events: none;
}
&.fade-in-widgets {
animation: none;
> * {
opacity: 0;
animation: fadeInWidget 0.8s ease-out forwards;
&:nth-child(1) {
animation-delay: 0.3s;
}
&:nth-child(2) {
animation-delay: 0.5s;
}
&:nth-child(3) {
animation-delay: 0.7s;
}
&:nth-child(4) {
animation-delay: 0.9s;
}
&:nth-child(5) {
animation-delay: 1.1s;
}
&:nth-child(6) {
animation-delay: 1.3s;
}
}
}
// Widgets visible behind welcome modal - no animation, just present
&.behind-welcome {
animation: none;
opacity: 1;
}
}
@keyframes fadeInWidget {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
&.no-textBorder {