diff --git a/src/features/background/options/sections/SourceSection.jsx b/src/features/background/options/sections/SourceSection.jsx index dfc6f78f..2ba748d6 100644 --- a/src/features/background/options/sections/SourceSection.jsx +++ b/src/features/background/options/sections/SourceSection.jsx @@ -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 ? : null, - )} )} diff --git a/src/features/marketplace/components/Items/Items.jsx b/src/features/marketplace/components/Items/Items.jsx index f4154684..059d98c8 100644 --- a/src/features/marketplace/components/Items/Items.jsx +++ b/src/features/marketplace/components/Items/Items.jsx @@ -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 (
toggleFunction(item)} + onClick={handleCardClick} key={item.name} > {isAdded && onTogglePack && ( @@ -207,18 +219,38 @@ function ItemCard({ )} {isAdded && onUninstall && ( -
)} + {isAdded && ( + setShowSettingsModal(false)} + isEnabled={isEnabled} + /> + )} ); } diff --git a/src/features/marketplace/components/Modals/ItemSettingsModal.jsx b/src/features/marketplace/components/Modals/ItemSettingsModal.jsx new file mode 100644 index 00000000..b55eea28 --- /dev/null +++ b/src/features/marketplace/components/Modals/ItemSettingsModal.jsx @@ -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 ( + handleSettingChange(field.key, newValue)} + /> + ); + } + + case 'chipselect': { + const options = field.dynamic ? dynamicOptions[field.key] || [] : field.options; + return ( + handleSettingChange(field.key, newValue)} + /> + ); + } + + case 'text': + return ( + handleSettingChange(field.key, e.target.value, field.secure)} + subtitle={field.help_text} + /> + ); + + case 'switch': + return ( + handleSettingChange(field.key, newValue)} + /> + ); + + case 'slider': + return ( + 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 ( + +
+
+ {pack.icon_url ? ( + icon { + e.target.onerror = null; + e.target.src = placeholderIcon; + }} + /> + ) : ( +
+ {(pack.display_name || pack.name)?.substring(0, 2).toUpperCase()} +
+ )} +
+

{pack.display_name || pack.name}

+ + {pack.author && `by ${pack.author}`} + {pack.version && v{pack.version}} + + {isEnabled ? : } + {isEnabled ? 'Enabled' : 'Disabled'} + + +
+
+ +
+ +
+ {hasSettings && ( + <> + {validationErrors.length > 0 && ( +
+ + Configuration incomplete: {validationErrors.join(', ')} +
+ )} + +
+ {pack.settings_schema.map((field) => ( +
+ {renderField(field)} +
+ ))} +
+ + {isPhotoPack && ( +
+
+ )} + + )} + + {!hasSettings && ( +
+
+ Type + + {variables.getMessage( + 'modals.main.marketplace.' + getTypeTranslationKey(pack.type), + )} + +
+ {pack.description && ( +
+ Description + {pack.description} +
+ )} + {pack.sideload && ( +
+ Source + Sideloaded +
+ )} +
+ )} +
+
+ ); +}; + +function getTypeTranslationKey(type) { + const typeMap = { + photos: 'photo_packs', + quotes: 'quote_packs', + settings: 'preset_settings', + }; + return typeMap[type] || type; +} + +export default ItemSettingsModal; diff --git a/src/features/marketplace/components/Modals/ItemSettingsModal.scss b/src/features/marketplace/components/Modals/ItemSettingsModal.scss new file mode 100644 index 00000000..7f61b2c2 --- /dev/null +++ b/src/features/marketplace/components/Modals/ItemSettingsModal.scss @@ -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)); + } +}