mirror of
https://github.com/mue/mue.git
synced 2026-07-15 13:03:55 +02:00
feat: library display option, navbar count
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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}>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as ModalLoader } from './ModalLoader';
|
||||
export { default as ModalNavbar } from './ModalNavbar';
|
||||
export { default as ReminderInfo } from './ReminderInfo';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user