feat: library display option, navbar count

This commit is contained in:
David Ralph
2026-01-28 15:49:22 +00:00
parent 05bf8edeea
commit a4e575c5f6
10 changed files with 248 additions and 48 deletions

View File

@@ -3,7 +3,7 @@ import Tooltip from 'components/Elements/Tooltip/Tooltip';
const Button = forwardRef(
(
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href, style },
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href, style, badge },
ref,
) => {
let className;
@@ -46,16 +46,18 @@ const Button = forwardRef(
<button className={className} onClick={onClick} ref={ref} disabled={disabled} style={style}>
{icon}
{label}
{badge !== undefined && badge !== null && <span className="btn-badge">{badge}</span>}
</button>
);
const linkButton = (
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href} style={style}
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href} style={style}
target="_blank"
rel="noopener noreferrer"
>
{icon}
{label}
{badge && <span className="btn-badge">{badge}</span>}
</a>
);
@@ -73,6 +75,7 @@ const Button = forwardRef(
>
{icon}
{label}
{badge && <span className="btn-badge">{badge}</span>}
</a>
</Tooltip>
);

View File

@@ -1,20 +0,0 @@
import variables from 'config/variables';
import { Button } from 'components/Elements';
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
const ModalNavbar = ({ currentTab, onChangeTab }) => (
<div className="modalNavbar">
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => (
<Button
key={tab}
type="navigation"
onClick={() => onChangeTab(tab)}
icon={<Icon />}
label={variables.getMessage(messageKey)}
active={currentTab === tab}
/>
))}
</div>
);
export default ModalNavbar;

View File

@@ -1,7 +1,8 @@
import { useT } from 'contexts/TranslationContext';
import { useState, useEffect } from 'react';
import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md';
import { Tooltip, Button } from 'components/Elements';
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
import { NAVBAR_BUTTONS, TAB_TYPES } from '../constants/tabConfig';
import mueAboutIcon from 'assets/icons/mue_about.png';
// Map marketplace types to translation keys
@@ -33,6 +34,38 @@ function ModalTopBar({
}) {
const t = useT();
// Track installed addons count for badge
const [installedCount, setInstalledCount] = useState(() => {
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
return installed.length;
} catch (e) {
return 0;
}
});
useEffect(() => {
const updateCount = () => {
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
setInstalledCount(installed.length);
} catch (e) {
setInstalledCount(0);
}
};
// Listen for storage events (changes from other tabs)
window.addEventListener('storage', updateCount);
// Listen for custom event for same-tab updates
window.addEventListener('installedAddonsChanged', updateCount);
return () => {
window.removeEventListener('storage', updateCount);
window.removeEventListener('installedAddonsChanged', updateCount);
};
}, []);
// Get the current tab label
const currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab);
const currentTabLabel = currentTabButton ? t(currentTabButton.messageKey) : '';
@@ -204,16 +237,22 @@ function ModalTopBar({
</div>
<div className="topBarRight">
<div className="topBarNavigation">
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => (
<Button
key={tab}
type="navigation"
onClick={() => onTabChange(tab)}
active={currentTab === tab}
icon={<Icon />}
label={t(messageKey)}
/>
))}
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => {
// Show badge for Library tab when there are installed addons
const badgeValue = tab === TAB_TYPES.LIBRARY && installedCount > 0 ? installedCount : undefined;
return (
<Button
key={tab}
type="navigation"
onClick={() => onTabChange(tab)}
active={currentTab === tab}
icon={<Icon />}
label={t(messageKey)}
badge={badgeValue}
/>
);
})}
</div>
<Tooltip title={t('modals.welcome.buttons.close')} key="closeTooltip">
<span className="closeModal" onClick={onClose}>

View File

@@ -1,3 +1,2 @@
export { default as ModalLoader } from './ModalLoader';
export { default as ModalNavbar } from './ModalNavbar';
export { default as ReminderInfo } from './ReminderInfo';

View File

@@ -46,6 +46,10 @@
border-radius: 12px;
transition: 0.5s;
@include themed {
background-color: t($modal-sidebarActive);
}
&.item-icon-text {
display: flex;
align-items: center;
@@ -439,3 +443,100 @@ p.author {
}
}
}
.view-toggle-buttons {
display: flex;
gap: 8px;
align-items: center;
.view-toggle-btn {
all: unset;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
-webkit-user-select: none;
user-select: none;
@include themed {
background-color: t($modal-sidebarActive);
color: t($subColor);
}
svg {
font-size: 20px;
}
&:hover {
@include themed {
background-color: rgba(255, 255, 255, 0.15);
color: t($color);
}
}
&.active {
@include themed {
background-color: #fff;
color: #000;
}
&:hover {
@include themed {
background-color: #f0f0f0;
}
}
}
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
}
}
.items-list {
display: flex !important;
flex-direction: column !important;
grid-template-columns: unset !important;
gap: 12px !important;
.item {
flex-direction: row !important;
align-items: center !important;
padding: 1rem 1.5rem !important;
gap: 20px !important;
&:hover {
transform: translate3d(5px, 0, 0) !important;
}
.item-icon {
flex-shrink: 0;
}
.card-details {
flex: 1;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between;
gap: 15px;
.card-title {
font-size: 16px;
}
.card-subtitle {
font-size: 13px;
}
.card-chips {
margin-top: 0 !important;
margin-left: auto;
}
}
}
}

View File

@@ -98,6 +98,51 @@
color: t($color);
}
}
.btn-badge {
margin-left: 3px;
padding: 5px 7px;
border-radius: 6px;
font-size: 0.75em !important;
font-weight: 700;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
@include themed {
background-color: t($modal-sidebarActive);
color: t($color);
}
.light & {
background-color: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.8);
}
.dark & {
background-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.9);
}
}
&.btn-navigation-active .btn-badge {
@include themed {
background-color: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.8);
}
.light & {
background-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.85);
}
.dark & {
background-color: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 1);
}
}
}
/* safari fix */

View File

@@ -84,14 +84,14 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
{isSideloaded && (
<Tooltip
title={variables.getMessage('modals.main.addons.sideload.title')}
style={{ position: 'absolute', top: '12px', right: '48px', zIndex: 2 }}
style={{ position: 'absolute', top: '12px', right: isAdded ? '48px' : '12px', zIndex: 2 }}
>
<div className="item-sideload-badge">
<MdOutlineUploadFile />
</div>
</Tooltip>
)}
{isInstalled && item.colour && !isSideloaded && (
{isInstalled && item.colour && !isSideloaded && !isAdded && (
<div className="item-installed-badge">
<MdCheckCircle />
</div>
@@ -148,6 +148,7 @@ function Items({
onSortChange,
isAdded = false,
onUninstall,
viewType = 'grid',
}) {
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z');
@@ -200,7 +201,7 @@ function Items({
/>
</div>
)}
<div className='items'>
<div className={`items ${viewType === 'list' ? 'items-list' : 'items-grid'}`}>
{items
?.filter((item) => filterItems(item, filter, filterOptions ? selectedCategory : 'all'))
.map((item, index) => (

View File

@@ -14,6 +14,7 @@ export const useMarketplaceInstall = () => {
toast(variables.getMessage('toasts.installed'));
variables.stats.postEvent('marketplace-item', `${data.display_name || data.name} installed`);
variables.stats.postEvent('marketplace', 'Install');
window.dispatchEvent(new Event('installedAddonsChanged'));
};
const uninstallItem = (type, name) => {
@@ -21,6 +22,7 @@ export const useMarketplaceInstall = () => {
toast(variables.getMessage('toasts.uninstalled'));
variables.stats.postEvent('marketplace-item', `${name} uninstalled`);
variables.stats.postEvent('marketplace', 'Uninstall');
window.dispatchEvent(new Event('installedAddonsChanged'));
};
const installCollection = async (items) => {
@@ -51,6 +53,7 @@ export const useMarketplaceInstall = () => {
}
toast(variables.getMessage('toasts.installed'));
window.dispatchEvent(new Event('installedAddonsChanged'));
window.location.reload();
} catch (error) {
if (!controllerRef.current.signal.aborted) {

View File

@@ -1,6 +1,6 @@
import variables from 'config/variables';
import { memo, useState, useEffect, useCallback } from 'react';
import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension, MdExplore } from 'react-icons/md';
import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension, MdExplore, MdViewModule, MdViewList } from 'react-icons/md';
import { toast } from 'react-toastify';
import Modal from 'react-modal';
@@ -17,6 +17,7 @@ const Added = memo(() => {
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
const [showFailed, setShowFailed] = useState(false);
const [failedReason, setFailedReason] = useState('');
const [viewType, setViewType] = useState(localStorage.getItem('addonsViewType') || 'grid');
const installAddon = useCallback((input) => {
let failedReasonText = '';
@@ -55,6 +56,7 @@ const Added = memo(() => {
toast(variables.getMessage('toasts.installed'));
variables.stats.postEvent('marketplace', 'Sideload');
setInstalled(JSON.parse(localStorage.getItem('installed')));
window.dispatchEvent(new Event('installedAddonsChanged'));
}, []);
const getSideloadButton = useCallback(() => {
@@ -149,12 +151,14 @@ const Added = memo(() => {
localStorage.setItem('installed', JSON.stringify([]));
toast(variables.getMessage('toasts.uninstalled_all'));
setInstalled([]);
window.dispatchEvent(new Event('installedAddonsChanged'));
}, [installed]);
const handleUninstall = useCallback((type, name) => {
uninstall(type, name);
toast(variables.getMessage('toasts.uninstalled'));
setInstalled(JSON.parse(localStorage.getItem('installed')));
window.dispatchEvent(new Event('installedAddonsChanged'));
}, []);
useEffect(() => {
@@ -192,6 +196,11 @@ const Added = memo(() => {
window.dispatchEvent(event);
}, []);
const toggleViewType = useCallback((type) => {
setViewType(type);
localStorage.setItem('addonsViewType', type);
}, []);
if (installed.length === 0) {
return (
<>
@@ -240,16 +249,34 @@ const Added = memo(() => {
/>
</CustomActions>
</Header>
<Dropdown
label={variables.getMessage('modals.main.addons.sort.title')}
name="sortAddons"
onChange={(value) => sortAddons(value)}
items={[
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
{ value: 'recently-updated', text: 'Recently Updated' },
]}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '15px', marginBottom: '15px' }}>
<Dropdown
label={variables.getMessage('modals.main.addons.sort.title')}
name="sortAddons"
onChange={(value) => sortAddons(value)}
items={[
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
{ value: 'recently-updated', text: 'Recently Updated' },
]}
/>
<div className="view-toggle-buttons">
<button
className={`view-toggle-btn ${viewType === 'grid' ? 'active' : ''}`}
onClick={() => toggleViewType('grid')}
aria-label="Grid view"
>
<MdViewModule />
</button>
<button
className={`view-toggle-btn ${viewType === 'list' ? 'active' : ''}`}
onClick={() => toggleViewType('list')}
aria-label="List view"
>
<MdViewList />
</button>
</div>
</div>
<Items
items={installed}
isAdded={true}
@@ -257,6 +284,7 @@ const Added = memo(() => {
toggleFunction={(input) => toggle('item', input)}
showCreateYourOwn={false}
onUninstall={handleUninstall}
viewType={viewType}
/>
</>
);

View File

@@ -40,6 +40,7 @@ const tryInstallDefaultPack = async () => {
);
const { data } = await response.json();
install(data.type, data, false, true);
window.dispatchEvent(new Event('installedAddonsChanged'));
return true;
} catch (e) {
console.error('Failed to install default pack:', e);