mirror of
https://github.com/mue/mue.git
synced 2026-06-06 07:55:48 +02:00
feat: new default quotes experience, improve added page
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 280px));
|
||||
grid-gap: 1.5rem;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
@@ -62,6 +62,20 @@
|
||||
width: 60px !important;
|
||||
border-radius: 12px;
|
||||
transition: 0.5s;
|
||||
|
||||
&.item-icon-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-sidebarActive);
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-details {
|
||||
@@ -113,6 +127,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.item-uninstall-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(220, 50, 50, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.item-installed-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
@@ -135,9 +171,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.item-sideload-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(100, 100, 100, 0.9);
|
||||
cursor: help;
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .item-installed-badge {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&.item-sideloaded {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import variables from 'config/variables';
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
import { MdAutoFixHigh, MdOutlineArrowForward, MdOutlineOpenInNew, MdCheckCircle } from 'react-icons/md';
|
||||
import {
|
||||
MdAutoFixHigh,
|
||||
MdOutlineArrowForward,
|
||||
MdOutlineOpenInNew,
|
||||
MdCheckCircle,
|
||||
MdOutlineUploadFile,
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
|
||||
|
||||
import { Button } from 'components/Elements';
|
||||
import { Button, Tooltip } from 'components/Elements';
|
||||
import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown';
|
||||
|
||||
function filterItems(item, filter, categoryFilter) {
|
||||
@@ -28,7 +35,29 @@ function filterItems(item, filter, categoryFilter) {
|
||||
return textMatch && item.type === categoryMap[categoryFilter];
|
||||
}
|
||||
|
||||
function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled }) {
|
||||
function getInitials(name) {
|
||||
if (!name) return '??';
|
||||
const words = name.split(' ');
|
||||
if (words.length === 1) {
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
return words
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function getTypeTranslationKey(type) {
|
||||
const typeMap = {
|
||||
photos: 'photo_packs',
|
||||
quotes: 'quote_packs',
|
||||
settings: 'preset_settings',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled, isAdded, onUninstall }) {
|
||||
item._onCollection = onCollection;
|
||||
|
||||
// Convert hex color to RGB for gradient with opacity
|
||||
@@ -73,28 +102,62 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
|
||||
};
|
||||
};
|
||||
|
||||
const isSideloaded = item.sideload === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="item"
|
||||
onClick={() => toggleFunction(item)}
|
||||
className={`item ${isSideloaded ? 'item-sideloaded' : ''}`}
|
||||
onClick={isSideloaded ? undefined : () => toggleFunction(item)}
|
||||
key={item.name}
|
||||
style={getGradientStyle()}
|
||||
>
|
||||
{isInstalled && item.colour && (
|
||||
{isAdded && onUninstall && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
style={{ position: 'absolute', top: '12px', right: '12px', zIndex: 3 }}
|
||||
>
|
||||
<button
|
||||
className="item-uninstall-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUninstall(item.type, item.name);
|
||||
}}
|
||||
>
|
||||
<MdClose />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isSideloaded && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.addons.sideload.title')}
|
||||
style={{ position: 'absolute', top: '12px', right: '48px', zIndex: 2 }}
|
||||
>
|
||||
<div className="item-sideload-badge">
|
||||
<MdOutlineUploadFile />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isInstalled && item.colour && !isSideloaded && (
|
||||
<div className="item-installed-badge" style={getBadgeStyle()}>
|
||||
<MdCheckCircle />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
className="item-icon"
|
||||
alt="icon"
|
||||
draggable={false}
|
||||
src={item.icon_url}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = placeholderIcon;
|
||||
}}
|
||||
/>
|
||||
{item.icon_url ? (
|
||||
<img
|
||||
className="item-icon"
|
||||
alt="icon"
|
||||
draggable={false}
|
||||
src={item.icon_url}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = placeholderIcon;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="item-icon item-icon-text">
|
||||
{getInitials(item.display_name || item.name)}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-details">
|
||||
<span className="card-title">{item.display_name || item.name}</span>
|
||||
{!isCurator ? (
|
||||
@@ -106,17 +169,14 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
|
||||
)}
|
||||
|
||||
<div className="card-chips">
|
||||
{type === 'all' && !onCollection ? (
|
||||
{item.type && (
|
||||
<span className="card-type">
|
||||
{variables.getMessage('modals.main.marketplace.' + item.type)}
|
||||
{variables.getMessage('modals.main.marketplace.' + getTypeTranslationKey(item.type))}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{/* {item.in_collections && item.in_collections.length > 0 && !onCollection ? (
|
||||
<span className="card-collection">
|
||||
{item.in_collections[0]}
|
||||
</span>
|
||||
) : null} */}
|
||||
)}
|
||||
{item.in_collections && item.in_collections.length > 0 && !onCollection && (
|
||||
<span className="card-collection">{item.in_collections[0]}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,6 +196,8 @@ function Items({
|
||||
showCreateYourOwn,
|
||||
filterOptions = false,
|
||||
onSortChange,
|
||||
isAdded = false,
|
||||
onUninstall,
|
||||
}) {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z');
|
||||
@@ -239,6 +301,8 @@ function Items({
|
||||
type={type}
|
||||
onCollection={onCollection}
|
||||
isInstalled={installedNames.has(item.name)}
|
||||
isAdded={isAdded}
|
||||
onUninstall={onUninstall}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -84,27 +84,24 @@ const Added = memo(() => {
|
||||
|
||||
const sortAddons = useCallback((value, sendEvent) => {
|
||||
const installedItems = JSON.parse(localStorage.getItem('installed'));
|
||||
|
||||
|
||||
switch (value) {
|
||||
case 'newest':
|
||||
installedItems.reverse();
|
||||
break;
|
||||
case 'oldest':
|
||||
break;
|
||||
case 'a-z':
|
||||
installedItems.sort((a, b) => {
|
||||
if (a.display_name < b.display_name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.display_name > b.display_name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
const nameA = (a.display_name || a.name || '').toLowerCase();
|
||||
const nameB = (b.display_name || b.name || '').toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
break;
|
||||
case 'z-a':
|
||||
installedItems.sort();
|
||||
installedItems.reverse();
|
||||
case 'recently-updated':
|
||||
installedItems.sort((a, b) => {
|
||||
const dateA = a.updated_at ? new Date(a.updated_at) : new Date(0);
|
||||
const dateB = b.updated_at ? new Date(b.updated_at) : new Date(0);
|
||||
return dateB - dateA;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -154,6 +151,12 @@ const Added = memo(() => {
|
||||
setInstalled([]);
|
||||
}, [installed]);
|
||||
|
||||
const handleUninstall = useCallback((type, name) => {
|
||||
uninstall(type, name);
|
||||
toast(variables.getMessage('toasts.uninstalled'));
|
||||
setInstalled(JSON.parse(localStorage.getItem('installed')));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sortAddons(localStorage.getItem('sortAddons'), false);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
@@ -243,9 +246,8 @@ const Added = memo(() => {
|
||||
onChange={(value) => sortAddons(value)}
|
||||
items={[
|
||||
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
|
||||
{ value: 'oldest', text: variables.getMessage('modals.main.addons.sort.oldest') },
|
||||
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
|
||||
{ value: 'z-a', text: variables.getMessage('modals.main.addons.sort.z_a') },
|
||||
{ value: 'recently-updated', text: 'Recently Updated' },
|
||||
]}
|
||||
/>
|
||||
<Items
|
||||
@@ -254,6 +256,7 @@ const Added = memo(() => {
|
||||
filter=""
|
||||
toggleFunction={(input) => toggle('item', input)}
|
||||
showCreateYourOwn={false}
|
||||
onUninstall={handleUninstall}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,45 @@ import Preview from '../../helpers/preview/Preview';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { parseDeepLink, shouldAutoOpenModal, updateHash } from 'utils/deepLinking';
|
||||
import { install } from 'utils/marketplace';
|
||||
|
||||
import Welcome from 'features/welcome/Welcome';
|
||||
|
||||
const DEFAULT_PACK_ID = '0c8a5bdebd13';
|
||||
|
||||
const isDefaultPackInstalled = () => {
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
return installed.some((item) => item.id === DEFAULT_PACK_ID);
|
||||
};
|
||||
|
||||
const isDefaultPackUninstalled = () => {
|
||||
const uninstalledPacks = JSON.parse(localStorage.getItem('uninstalledPacks') || '[]');
|
||||
return uninstalledPacks.includes(DEFAULT_PACK_ID);
|
||||
};
|
||||
|
||||
const tryInstallDefaultPack = async () => {
|
||||
// Don't install if offline mode, already installed, or explicitly uninstalled
|
||||
if (
|
||||
localStorage.getItem('offlineMode') === 'true' ||
|
||||
isDefaultPackInstalled() ||
|
||||
isDefaultPackUninstalled()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${variables.constants.API_URL}/marketplace/item/${DEFAULT_PACK_ID}`,
|
||||
);
|
||||
const { data } = await response.json();
|
||||
install(data.type, data, false, true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to install default pack:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const Modals = () => {
|
||||
const [mainModal, setMainModal] = useState(false);
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
@@ -60,6 +96,15 @@ const Modals = () => {
|
||||
localStorage.setItem('showReminder', false);
|
||||
}
|
||||
|
||||
// Try to install default pack if it wasn't installed during welcome (e.g., no internet)
|
||||
if (localStorage.getItem('showWelcome') !== 'true') {
|
||||
tryInstallDefaultPack().then((installed) => {
|
||||
if (installed) {
|
||||
EventBus.emit('refresh', 'quote');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for EventBus modal open requests
|
||||
const handleModalOpen = (data) => {
|
||||
if (data === 'openMainModal') {
|
||||
@@ -76,9 +121,12 @@ const Modals = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const closeWelcome = () => {
|
||||
const closeWelcome = async () => {
|
||||
localStorage.setItem('showWelcome', false);
|
||||
setWelcomeModal(false);
|
||||
|
||||
await tryInstallDefaultPack();
|
||||
|
||||
EventBus.emit('refresh', 'widgetsWelcomeDone');
|
||||
EventBus.emit('refresh', 'widgets');
|
||||
EventBus.emit('refresh', 'backgroundwelcome');
|
||||
|
||||
@@ -83,7 +83,13 @@ export function useQuoteLoader(updateQuote) {
|
||||
|
||||
const getQuote = useCallback(async () => {
|
||||
const offline = localStorage.getItem('offlineMode') === 'true';
|
||||
const type = localStorage.getItem('quoteType') || 'api';
|
||||
let type = localStorage.getItem('quoteType') || 'quote_pack';
|
||||
|
||||
// Migrate deprecated 'api' type to 'quote_pack'
|
||||
if (type === 'api') {
|
||||
type = 'quote_pack';
|
||||
localStorage.setItem('quoteType', 'quote_pack');
|
||||
}
|
||||
|
||||
// Check for favourite quote first
|
||||
const favouriteQuote = localStorage.getItem('favouriteQuote');
|
||||
@@ -128,7 +134,8 @@ export function useQuoteLoader(updateQuote) {
|
||||
});
|
||||
}
|
||||
|
||||
case 'quote_pack': {
|
||||
case 'quote_pack':
|
||||
default: {
|
||||
if (offline) return doOffline();
|
||||
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
@@ -138,56 +145,31 @@ export function useQuoteLoader(updateQuote) {
|
||||
...quote,
|
||||
fallbackauthorimg: item.icon_url,
|
||||
packName: item.display_name || item.name,
|
||||
noAuthorImg: item.noAuthorImg || quote.noAuthorImg,
|
||||
})));
|
||||
|
||||
if (quotePack.length === 0) return doOffline();
|
||||
|
||||
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
|
||||
const hasAuthor = data.author && data.author.trim() !== '';
|
||||
const displayAuthor = hasAuthor ? data.author : data.packName;
|
||||
|
||||
// Try to get author image from Wikipedia unless pack disables it
|
||||
let authorimgdata = { authorimg: data.fallbackauthorimg, authorimglicense: null };
|
||||
if (hasAuthor && !data.noAuthorImg) {
|
||||
const wikiImg = await getAuthorImg(data.author);
|
||||
if (wikiImg.authorimg) {
|
||||
authorimgdata = wikiImg;
|
||||
}
|
||||
}
|
||||
|
||||
return updateQuote({
|
||||
quote: `"${data.quote}"`,
|
||||
author: hasAuthor ? data.author : data.packName,
|
||||
author: displayAuthor,
|
||||
authorlink: hasAuthor ? getAuthorLink(data.author) : null,
|
||||
authorimg: data.fallbackauthorimg,
|
||||
...authorimgdata,
|
||||
});
|
||||
}
|
||||
|
||||
case 'api': {
|
||||
if (offline) return doOffline();
|
||||
|
||||
const fetchAPIQuote = async () => {
|
||||
const response = await fetch(
|
||||
`${variables.constants.API_URL}/quotes/random`
|
||||
).then(res => res.json());
|
||||
|
||||
if (response.statusCode === 429) return null;
|
||||
|
||||
const authorimgdata = await getAuthorImg(response.author);
|
||||
return {
|
||||
quote: `"${response.quote.replace(/\s+$/g, '')}"`,
|
||||
author: response.author,
|
||||
authorlink: getAuthorLink(response.author),
|
||||
...authorimgdata,
|
||||
authorOccupation: response.author_occupation,
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem('nextQuote')) || await fetchAPIQuote();
|
||||
localStorage.setItem('nextQuote', null);
|
||||
|
||||
if (data) {
|
||||
updateQuote(data);
|
||||
localStorage.setItem('currentQuote', JSON.stringify(data));
|
||||
localStorage.setItem('nextQuote', JSON.stringify(await fetchAPIQuote()));
|
||||
} else {
|
||||
doOffline();
|
||||
}
|
||||
} catch {
|
||||
doOffline();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [updateQuote, getAuthorLink, getAuthorImg, doOffline]);
|
||||
|
||||
|
||||
@@ -23,7 +23,15 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
|
||||
return data;
|
||||
};
|
||||
|
||||
const [quoteType, setQuoteType] = useState(localStorage.getItem('quoteType') || 'api');
|
||||
const [quoteType, setQuoteType] = useState(() => {
|
||||
let type = localStorage.getItem('quoteType') || 'quote_pack';
|
||||
// Migrate deprecated 'api' type to 'quote_pack'
|
||||
if (type === 'api') {
|
||||
type = 'quote_pack';
|
||||
localStorage.setItem('quoteType', 'quote_pack');
|
||||
}
|
||||
return type;
|
||||
});
|
||||
const [customQuote, setCustomQuote] = useState(getCustom());
|
||||
|
||||
const handleCustomQuote = (e, text, index, type) => {
|
||||
@@ -93,10 +101,6 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
|
||||
value: 'quote_pack',
|
||||
text: variables.getMessage('modals.main.marketplace.title'),
|
||||
},
|
||||
{
|
||||
value: 'api',
|
||||
text: variables.getMessage('modals.main.settings.sections.background.type.api'),
|
||||
},
|
||||
{ value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -599,10 +599,9 @@
|
||||
},
|
||||
"sort": {
|
||||
"title": "Sort",
|
||||
"newest": "Installed (Newest)",
|
||||
"oldest": "Installed (Oldest)",
|
||||
"a_z": "Alphabetical (A-Z)",
|
||||
"z_a": "Alphabetical (Z-A)"
|
||||
"newest": "Recently Added",
|
||||
"a_z": "Name (A-Z)",
|
||||
"recently_updated": "Recently Updated"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create",
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
},
|
||||
{
|
||||
"name": "quoteType",
|
||||
"value": "api"
|
||||
"value": "quote_pack"
|
||||
},
|
||||
{
|
||||
"name": "backgroundFilter",
|
||||
|
||||
@@ -76,6 +76,14 @@ export function uninstall(type, name) {
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
for (let i = 0; i < installed.length; i++) {
|
||||
if (installed[i].name === name) {
|
||||
// Track uninstalled pack IDs to prevent auto-reinstall
|
||||
if (installed[i].id) {
|
||||
const uninstalledPacks = JSON.parse(localStorage.getItem('uninstalledPacks') || '[]');
|
||||
if (!uninstalledPacks.includes(installed[i].id)) {
|
||||
uninstalledPacks.push(installed[i].id);
|
||||
localStorage.setItem('uninstalledPacks', JSON.stringify(uninstalledPacks));
|
||||
}
|
||||
}
|
||||
installed.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user