Merge branch 'beta' into dev

Signed-off-by: Alex Sparkes <alexsparkes@gmail.com>
This commit is contained in:
Alex Sparkes
2026-01-27 12:10:35 +00:00
committed by GitHub
7 changed files with 221 additions and 196 deletions

View File

@@ -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%;

View File

@@ -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>
);
});

View File

@@ -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;
}
}
}

View File

@@ -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>
)}

View File

@@ -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) => {