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 ? (
+
})
{
+ 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 && (
+
+ }
+ label={variables.getMessage(
+ 'modals.main.settings.sections.background.photo_pack_settings.refresh_photos',
+ )}
+ disabled={isRefreshing || validationErrors.length > 0}
+ />
+
+ )}
+ >
+ )}
+
+ {!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));
+ }
+}