mirror of
https://github.com/mue/mue.git
synced 2026-07-21 16:04:22 +02:00
Merge branch 'beta' into dev
Signed-off-by: Alex Sparkes <alexsparkes@gmail.com>
This commit is contained in:
@@ -184,6 +184,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
.itemPage {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.itemContent {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 25px;
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
word-wrap: break-word !important;
|
||||
font-size: 16px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.itemTop {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyItems {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { memo, useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { MdExpandMore, MdCheck, MdRefresh } from 'react-icons/md';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -9,51 +8,25 @@ import EventBus from 'utils/eventbus';
|
||||
import './Dropdown.scss';
|
||||
|
||||
const Dropdown = memo((props) => {
|
||||
const [value, setValue] = useState(localStorage.getItem(props.name) || props.items[0]?.value);
|
||||
const [value, setValue] = useState(
|
||||
localStorage.getItem(props.name) || props.items[0]?.value,
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const containerRef = useRef(null);
|
||||
const controlRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
const optionsRef = useRef([]);
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
setIsClosing(false);
|
||||
setFocusedIndex(-1);
|
||||
}, 200); // Match animation duration
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target) &&
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target)
|
||||
) {
|
||||
closeDropdown();
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
setFocusedIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [closeDropdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && controlRef.current) {
|
||||
const rect = controlRef.current.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback(
|
||||
(newValue) => {
|
||||
@@ -64,7 +37,8 @@ const Dropdown = memo((props) => {
|
||||
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
|
||||
|
||||
setValue(newValue);
|
||||
closeDropdown();
|
||||
setIsOpen(false);
|
||||
setFocusedIndex(-1);
|
||||
|
||||
if (!props.noSetting) {
|
||||
localStorage.setItem(props.name, newValue);
|
||||
@@ -95,23 +69,18 @@ const Dropdown = memo((props) => {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
break;
|
||||
case 'Escape':
|
||||
closeDropdown();
|
||||
setIsOpen(false);
|
||||
setFocusedIndex(-1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setFocusedIndex((prev) =>
|
||||
prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev,
|
||||
);
|
||||
setFocusedIndex((prev) => (prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev));
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
@@ -157,16 +126,8 @@ const Dropdown = memo((props) => {
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={controlRef}
|
||||
className="dropdown-control"
|
||||
onClick={() => {
|
||||
if (props.disabled) return;
|
||||
if (isOpen) {
|
||||
closeDropdown();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}}
|
||||
onClick={() => !props.disabled && setIsOpen(!isOpen)}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="button"
|
||||
aria-haspopup="listbox"
|
||||
@@ -177,39 +138,27 @@ const Dropdown = memo((props) => {
|
||||
<span className="dropdown-value">{selectedItem?.text || value}</span>
|
||||
<MdExpandMore className={`dropdown-arrow ${isOpen ? 'open' : ''}`} />
|
||||
</div>
|
||||
{(isOpen || isClosing) &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`dropdown-menu ${isClosing ? 'closing' : ''}`}
|
||||
role="listbox"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: `${menuPosition.top}px`,
|
||||
left: `${menuPosition.left}px`,
|
||||
width: `${menuPosition.width}px`,
|
||||
}}
|
||||
>
|
||||
{props.items.map((item, index) =>
|
||||
item !== null ? (
|
||||
<div
|
||||
key={id + item.value}
|
||||
ref={(el) => (optionsRef.current[index] = el)}
|
||||
className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`}
|
||||
onClick={() => onChange(item.value)}
|
||||
onKeyDown={(e) => handleOptionKeyDown(e, item)}
|
||||
role="option"
|
||||
aria-selected={value === item.value}
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className="dropdown-option-text">{item.text}</span>
|
||||
{value === item.value && <MdCheck className="dropdown-option-check" />}
|
||||
</div>
|
||||
) : null,
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="dropdown-menu" role="listbox">
|
||||
{props.items.map((item, index) =>
|
||||
item !== null ? (
|
||||
<div
|
||||
key={id + item.value}
|
||||
ref={(el) => (optionsRef.current[index] = el)}
|
||||
className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`}
|
||||
onClick={() => onChange(item.value)}
|
||||
onKeyDown={(e) => handleOptionKeyDown(e, item)}
|
||||
role="option"
|
||||
aria-selected={value === item.value}
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className="dropdown-option-text">{item.text}</span>
|
||||
{value === item.value && <MdCheck className="dropdown-option-check" />}
|
||||
</div>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,25 +13,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include keyframes(dropdownSlideOut) {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
margin-top: 10px;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
@@ -131,99 +121,98 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown menu and options are now portaled to body, so they need to be at root level
|
||||
.dropdown-menu {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
@include animation(dropdownSlideIn 0.2s ease-out);
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
@include animation(dropdownSlideIn 0.2s ease-out);
|
||||
|
||||
&.closing {
|
||||
@include animation(dropdownSlideOut 0.2s ease-out);
|
||||
}
|
||||
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
border-radius: 3px;
|
||||
background: t($modal-background);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@include themed {
|
||||
background: t($color);
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: t($modal-sidebar);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: t($modal-sidebarActive);
|
||||
border-left: 2px solid t($link);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-option-check {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include themed {
|
||||
background: t($link);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: t($modal-sidebar);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: t($modal-sidebarActive);
|
||||
border-left: 2px solid t($link);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-option-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-option-check {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include themed {
|
||||
background: t($link);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import variables from 'config/variables';
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
import {
|
||||
MdAutoFixHigh,
|
||||
MdOutlineArrowForward,
|
||||
MdOutlineOpenInNew,
|
||||
MdCheckCircle,
|
||||
MdOutlineUploadFile,
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
|
||||
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import { Button, Tooltip } from 'components/Elements';
|
||||
import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown';
|
||||
|
||||
function filterItems(item, filter, categoryFilter) {
|
||||
@@ -57,6 +60,40 @@ function getTypeTranslationKey(type) {
|
||||
function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled, isAdded, onUninstall }) {
|
||||
item._onCollection = onCollection;
|
||||
|
||||
// Convert hex color to RGB for gradient with opacity
|
||||
const hexToRgb = (hex) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
const getGradientStyle = () => {
|
||||
if (!item.colour) return {};
|
||||
|
||||
const rgb = hexToRgb(item.colour);
|
||||
if (!rgb) return {};
|
||||
|
||||
const baseColor = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
||||
|
||||
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;
|
||||
|
||||
const isSideloaded = item.sideload === true;
|
||||
|
||||
const isSideloaded = item.sideload === true;
|
||||
|
||||
return (
|
||||
@@ -92,7 +129,7 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
|
||||
</Tooltip>
|
||||
)}
|
||||
{isInstalled && item.colour && !isSideloaded && (
|
||||
<div className="item-installed-badge">
|
||||
<div className="item-installed-badge" style={getBadgeStyle()}>
|
||||
<MdCheckCircle />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -31,14 +31,6 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
|
||||
}
|
||||
return type;
|
||||
});
|
||||
|
||||
// Migration: Force authorDetails on for users upgrading from older versions
|
||||
useState(() => {
|
||||
if (localStorage.getItem('authorDetails') === null) {
|
||||
localStorage.setItem('authorDetails', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
const [customQuote, setCustomQuote] = useState(getCustom());
|
||||
|
||||
const handleCustomQuote = (e, text, index, type) => {
|
||||
|
||||
Reference in New Issue
Block a user