mirror of
https://github.com/mue/mue.git
synced 2026-07-15 04:53:48 +02:00
feat: add ItemSettingsModal for managing photo pack settings and enhance UI interactions
This commit is contained in:
@@ -5,7 +5,6 @@ import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
import { Button } from 'components/Elements';
|
||||
import Items from 'features/marketplace/components/Items/Items';
|
||||
import { getBackgroundOptionItems } from '../optionTypes';
|
||||
import PhotoPackSettings from './PhotoPackSettings';
|
||||
|
||||
const SourceSection = ({
|
||||
backgroundType,
|
||||
@@ -69,11 +68,6 @@ const SourceSection = ({
|
||||
viewType="grid"
|
||||
showChips={false}
|
||||
/>
|
||||
|
||||
{/* Settings for API packs */}
|
||||
{installedPhotoPacks.map((pack) =>
|
||||
pack.api_enabled ? <PhotoPackSettings key={pack.id} pack={pack} /> : null,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import variables from 'config/variables';
|
||||
import React, { memo, useState, useMemo } from 'react';
|
||||
import { MdCheckCircle, MdOutlineUploadFile, MdClose } from 'react-icons/md';
|
||||
import { MdCheckCircle, MdOutlineUploadFile, MdClose, MdSettings } from 'react-icons/md';
|
||||
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
|
||||
|
||||
import { Tooltip } from 'components/Elements';
|
||||
@@ -9,6 +9,7 @@ import Switch from 'components/Form/Settings/Switch/Switch';
|
||||
import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { getProxiedImageUrl } from 'utils/marketplace';
|
||||
import ItemSettingsModal from '../Modals/ItemSettingsModal';
|
||||
|
||||
function filterItems(item, filter, categoryFilter) {
|
||||
const lowerCaseFilter = filter.toLowerCase();
|
||||
@@ -68,6 +69,8 @@ function ItemCard({
|
||||
}) {
|
||||
const isSideloaded = item.sideload === true;
|
||||
const packId = item.id || item.name;
|
||||
const isPhotoPack = item.type === 'photos' || item.type === 'photo_packs';
|
||||
const hasSettings = isPhotoPack && item.settings_schema && item.settings_schema.length > 0;
|
||||
|
||||
// Use React state to manage enabled status for immediate UI updates
|
||||
const [isEnabled, setIsEnabled] = useState(() => {
|
||||
@@ -75,6 +78,15 @@ function ItemCard({
|
||||
return enabledPacks[packId] !== false; // Default to enabled if not set
|
||||
});
|
||||
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (isSideloaded || showSettingsModal) {
|
||||
return;
|
||||
}
|
||||
toggleFunction(item);
|
||||
};
|
||||
|
||||
const handleTogglePack = (e) => {
|
||||
e.stopPropagation();
|
||||
const newState = !isEnabled;
|
||||
@@ -100,7 +112,7 @@ function ItemCard({
|
||||
return (
|
||||
<div
|
||||
className={`item ${isSideloaded ? 'item-sideloaded' : ''} ${!isEnabled && isAdded ? 'item-disabled' : ''}`}
|
||||
onClick={isSideloaded ? undefined : () => toggleFunction(item)}
|
||||
onClick={handleCardClick}
|
||||
key={item.name}
|
||||
>
|
||||
{isAdded && onTogglePack && (
|
||||
@@ -207,18 +219,38 @@ function ItemCard({
|
||||
)}
|
||||
|
||||
{isAdded && onUninstall && (
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUninstall(item.type, item.name);
|
||||
}}
|
||||
icon={<MdClose />}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
style={{ marginTop: '10px', width: '100%' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '10px', width: '100%' }}>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowSettingsModal(true);
|
||||
}}
|
||||
icon={<MdSettings />}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.settings')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUninstall(item.type, item.name);
|
||||
}}
|
||||
icon={<MdClose />}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAdded && (
|
||||
<ItemSettingsModal
|
||||
pack={item}
|
||||
isOpen={showSettingsModal}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
isEnabled={isEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
290
src/features/marketplace/components/Modals/ItemSettingsModal.jsx
Normal file
290
src/features/marketplace/components/Modals/ItemSettingsModal.jsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import variables from 'config/variables';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { Dropdown, Text, Switch, Slider, ChipSelect } from 'components/Form/Settings';
|
||||
import { Button } from 'components/Elements';
|
||||
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
|
||||
import { MdRefresh, MdWarning, MdClose, MdCheckCircle, MdCancel } from 'react-icons/md';
|
||||
import { getProxiedImageUrl } from 'utils/marketplace';
|
||||
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
|
||||
import './ItemSettingsModal.scss';
|
||||
|
||||
const ItemSettingsModal = ({ pack, isOpen, onClose, isEnabled }) => {
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const saved = localStorage.getItem(`photopack_settings_${pack.id}`);
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
});
|
||||
|
||||
const [dynamicOptions, setDynamicOptions] = useState({});
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState([]);
|
||||
|
||||
const validateSettings = useCallback(() => {
|
||||
const errors = [];
|
||||
pack.settings_schema?.forEach((field) => {
|
||||
if (field.required && !settings[field.key]) {
|
||||
errors.push(`${field.label} is required`);
|
||||
}
|
||||
});
|
||||
setValidationErrors(errors);
|
||||
|
||||
// Update api_packs_ready list
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
const isReady = errors.length === 0;
|
||||
const isInList = apiPacksReady.includes(pack.id);
|
||||
|
||||
if (isReady && !isInList) {
|
||||
apiPacksReady.push(pack.id);
|
||||
localStorage.setItem('api_packs_ready', JSON.stringify(apiPacksReady));
|
||||
} else if (!isReady && isInList) {
|
||||
const filtered = apiPacksReady.filter((id) => id !== pack.id);
|
||||
localStorage.setItem('api_packs_ready', JSON.stringify(filtered));
|
||||
}
|
||||
}, [pack.id, pack.settings_schema, settings]);
|
||||
|
||||
const loadDynamicOptions = async (field) => {
|
||||
if (field.options_source === 'api:categories') {
|
||||
try {
|
||||
const response = await fetch(`${variables.constants.API_URL}/images/categories`);
|
||||
const categories = await response.json();
|
||||
setDynamicOptions((prev) => ({
|
||||
...prev,
|
||||
[field.key]: categories,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Load dynamic options (e.g., categories from API)
|
||||
useEffect(() => {
|
||||
if (!pack.settings_schema || pack.settings_schema.length === 0) {
|
||||
return;
|
||||
}
|
||||
pack.settings_schema.forEach((field) => {
|
||||
if (field.dynamic && field.options_source) {
|
||||
loadDynamicOptions(field);
|
||||
}
|
||||
});
|
||||
}, [pack.id, pack.settings_schema]);
|
||||
|
||||
// Validate settings
|
||||
useEffect(() => {
|
||||
if (!pack.settings_schema || pack.settings_schema.length === 0) {
|
||||
return;
|
||||
}
|
||||
validateSettings();
|
||||
}, [settings, validateSettings, pack.settings_schema]);
|
||||
|
||||
const handleSettingChange = (key, value, secure = false) => {
|
||||
const processedValue = secure ? btoa(value) : value;
|
||||
const newSettings = { ...settings, [key]: processedValue };
|
||||
setSettings(newSettings);
|
||||
localStorage.setItem(`photopack_settings_${pack.id}`, JSON.stringify(newSettings));
|
||||
};
|
||||
|
||||
const handleManualRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await refreshAPIPackCache(pack.id);
|
||||
setIsRefreshing(false);
|
||||
// Trigger background refresh
|
||||
EventBus.emit('refresh', 'background');
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const value =
|
||||
field.secure && settings[field.key]
|
||||
? atob(settings[field.key])
|
||||
: settings[field.key] || field.default;
|
||||
|
||||
switch (field.type) {
|
||||
case 'dropdown': {
|
||||
const dropdownItems = field.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
text: opt.label,
|
||||
}));
|
||||
return (
|
||||
<Dropdown
|
||||
label={field.label}
|
||||
name={`${pack.id}_${field.key}`}
|
||||
value={value}
|
||||
items={dropdownItems}
|
||||
onChange={(newValue) => handleSettingChange(field.key, newValue)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'chipselect': {
|
||||
const options = field.dynamic ? dynamicOptions[field.key] || [] : field.options;
|
||||
return (
|
||||
<ChipSelect
|
||||
label={field.label}
|
||||
options={options}
|
||||
name={`${pack.id}_${field.key}`}
|
||||
onChange={(newValue) => handleSettingChange(field.key, newValue)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<Text
|
||||
title={field.label}
|
||||
placeholder={field.placeholder}
|
||||
value={value}
|
||||
name={`${pack.id}_${field.key}`}
|
||||
type={field.secure ? 'password' : 'text'}
|
||||
onChange={(e) => handleSettingChange(field.key, e.target.value, field.secure)}
|
||||
subtitle={field.help_text}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
name={`${pack.id}_${field.key}`}
|
||||
text={field.label}
|
||||
value={value}
|
||||
onChange={(newValue) => handleSettingChange(field.key, newValue)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'slider':
|
||||
return (
|
||||
<Slider
|
||||
title={field.label}
|
||||
name={`${pack.id}_${field.key}`}
|
||||
min={field.min || 0}
|
||||
max={field.max || 100}
|
||||
step={field.step || 1}
|
||||
value={value}
|
||||
onChange={(newValue) => handleSettingChange(field.key, newValue)}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const hasSettings = pack.settings_schema && pack.settings_schema.length > 0;
|
||||
const isPhotoPack = pack.type === 'photos' || pack.type === 'photo_packs';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={onClose}
|
||||
isOpen={isOpen}
|
||||
className="Modal photoPackSettingsModal"
|
||||
overlayClassName="Overlay photoPackSettingsOverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<div className="photoPackSettings-header">
|
||||
<div className="photoPackSettings-header-info">
|
||||
{pack.icon_url ? (
|
||||
<img
|
||||
className="photoPackSettings-icon"
|
||||
alt="icon"
|
||||
draggable={false}
|
||||
src={getProxiedImageUrl(pack.icon_url)}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = placeholderIcon;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="photoPackSettings-icon photoPackSettings-icon-text">
|
||||
{(pack.display_name || pack.name)?.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="photoPackSettings-header-text">
|
||||
<h2>{pack.display_name || pack.name}</h2>
|
||||
<span className="photoPackSettings-subtitle">
|
||||
{pack.author && `by ${pack.author}`}
|
||||
{pack.version && <span className="photoPackSettings-version">v{pack.version}</span>}
|
||||
<span className={`photoPackSettings-status ${isEnabled ? 'enabled' : 'disabled'}`}>
|
||||
{isEnabled ? <MdCheckCircle /> : <MdCancel />}
|
||||
{isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="photoPackSettings-close" onClick={onClose} aria-label="Close">
|
||||
<MdClose />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="photoPackSettings-content">
|
||||
{hasSettings && (
|
||||
<>
|
||||
{validationErrors.length > 0 && (
|
||||
<div className="photoPackSettings-error">
|
||||
<MdWarning />
|
||||
<span>Configuration incomplete: {validationErrors.join(', ')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="photoPackSettings-fields">
|
||||
{pack.settings_schema.map((field) => (
|
||||
<div key={field.key} className="photoPackSettings-field">
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isPhotoPack && (
|
||||
<div className="photoPackSettings-actions">
|
||||
<Button
|
||||
onClick={handleManualRefresh}
|
||||
icon={<MdRefresh />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.photo_pack_settings.refresh_photos',
|
||||
)}
|
||||
disabled={isRefreshing || validationErrors.length > 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasSettings && (
|
||||
<div className="photoPackSettings-info">
|
||||
<div className="photoPackSettings-info-item">
|
||||
<span className="label">Type</span>
|
||||
<span className="value">
|
||||
{variables.getMessage(
|
||||
'modals.main.marketplace.' + getTypeTranslationKey(pack.type),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{pack.description && (
|
||||
<div className="photoPackSettings-info-item">
|
||||
<span className="label">Description</span>
|
||||
<span className="value">{pack.description}</span>
|
||||
</div>
|
||||
)}
|
||||
{pack.sideload && (
|
||||
<div className="photoPackSettings-info-item">
|
||||
<span className="label">Source</span>
|
||||
<span className="value">Sideloaded</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
function getTypeTranslationKey(type) {
|
||||
const typeMap = {
|
||||
photos: 'photo_packs',
|
||||
quotes: 'quote_packs',
|
||||
settings: 'preset_settings',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
export default ItemSettingsModal;
|
||||
@@ -0,0 +1,182 @@
|
||||
.photoPackSettingsModal {
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background: var(--modal-background, #1a1a1a);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.photoPackSettingsOverlay {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.photoPackSettings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.photoPackSettings-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.photoPackSettings-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photoPackSettings-icon-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--linkColor, #ff5c25) 0%, #ff8a25 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.photoPackSettings-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.photoPackSettings-header-text h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #fff);
|
||||
}
|
||||
|
||||
.photoPackSettings-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--subtitle-color, rgba(255, 255, 255, 0.6));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.photoPackSettings-version {
|
||||
padding: 2px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.photoPackSettings-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.enabled {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: rgba(158, 158, 158, 0.2);
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.photoPackSettings-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color, #fff);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.photoPackSettings-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.photoPackSettings-content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.photoPackSettings-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: 8px;
|
||||
color: #f44336;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.photoPackSettings-error svg {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.photoPackSettings-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.photoPackSettings-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.photoPackSettings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.photoPackSettings-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.photoPackSettings-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--subtitle-color, rgba(255, 255, 255, 0.5));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: var(--text-color, rgba(255, 255, 255, 0.9));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user