refactor: rename photo pack settings to item settings in modal and styles

This commit is contained in:
alexsparkes
2026-02-03 16:36:37 +00:00
parent 7dec0a844e
commit ee89ebcd4d
3 changed files with 41 additions and 276 deletions

View File

@@ -1,235 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import variables from 'config/variables';
import EventBus from 'utils/eventbus';
import { Dropdown, Text, Switch, Slider, ChipSelect } from 'components/Form/Settings';
import { Row, Content, Action } from 'components/Layout/Settings/Item';
import { Button } from 'components/Elements';
import { Section } from 'components/Layout/Settings';
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
import { MdRefresh, MdWarning, MdExpandMore, MdExpandLess } from 'react-icons/md';
const PhotoPackSettings = ({ pack }) => {
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 [isExpanded, setIsExpanded] = useState(false);
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;
}
};
if (!pack.settings_schema || pack.settings_schema.length === 0) {
return null;
}
return (
<>
<Section
title={variables.getMessage(
'modals.main.settings.sections.background.photo_pack_settings.title',
{
name: pack.display_name || pack.name,
},
)}
subtitle={
pack.api_provider === 'mue'
? variables.getMessage('modals.main.settings.sections.background.source.api')
: variables.getMessage('modals.main.settings.sections.background.unsplash.subtitle')
}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <MdExpandLess /> : <MdExpandMore />}
</Section>
{isExpanded && (
<>
<Row>
<Content title="" />
<Action>
<Button
onClick={handleManualRefresh}
icon={<MdRefresh />}
label={variables.getMessage(
'modals.main.settings.sections.background.photo_pack_settings.refresh_photos',
)}
disabled={isRefreshing || validationErrors.length > 0}
/>
</Action>
</Row>
{validationErrors.length > 0 && (
<Row>
<Content>
<div
style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#f44336' }}
>
<MdWarning />
<span>Configuration incomplete: {validationErrors.join(', ')}</span>
</div>
</Content>
</Row>
)}
{pack.settings_schema.map((field, index) => (
<Row key={field.key} final={index === pack.settings_schema.length - 1}>
<Content title="" />
<Action>{renderField(field)}</Action>
</Row>
))}
</>
)}
</>
);
};
export default PhotoPackSettings;

View File

@@ -177,15 +177,15 @@ const ItemSettingsModal = ({ pack, isOpen, onClose, isEnabled }) => {
closeTimeoutMS={100}
onRequestClose={onClose}
isOpen={isOpen}
className="Modal photoPackSettingsModal"
overlayClassName="Overlay photoPackSettingsOverlay"
className="Modal itemSettingsModal"
overlayClassName="Overlay itemSettingsOverlay"
ariaHideApp={false}
>
<div className="photoPackSettings-header">
<div className="photoPackSettings-header-info">
<div className="itemSettings-header">
<div className="itemSettings-header-info">
{pack.icon_url ? (
<img
className="photoPackSettings-icon"
className="itemSettings-icon"
alt="icon"
draggable={false}
src={getProxiedImageUrl(pack.icon_url)}
@@ -195,47 +195,47 @@ const ItemSettingsModal = ({ pack, isOpen, onClose, isEnabled }) => {
}}
/>
) : (
<div className="photoPackSettings-icon photoPackSettings-icon-text">
<div className="itemSettings-icon itemSettings-icon-text">
{(pack.display_name || pack.name)?.substring(0, 2).toUpperCase()}
</div>
)}
<div className="photoPackSettings-header-text">
<div className="itemSettings-header-text">
<h2>{pack.display_name || pack.name}</h2>
<span className="photoPackSettings-subtitle">
<span className="itemSettings-subtitle">
{pack.author && `by ${pack.author}`}
{pack.version && <span className="photoPackSettings-version">v{pack.version}</span>}
<span className={`photoPackSettings-status ${isEnabled ? 'enabled' : 'disabled'}`}>
{pack.version && <span className="itemSettings-version">v{pack.version}</span>}
<span className={`itemSettings-status ${isEnabled ? 'enabled' : 'disabled'}`}>
{isEnabled ? <MdCheckCircle /> : <MdCancel />}
{isEnabled ? 'Enabled' : 'Disabled'}
</span>
</span>
</div>
</div>
<button className="photoPackSettings-close" onClick={onClose} aria-label="Close">
<button className="itemSettings-close" onClick={onClose} aria-label="Close">
<MdClose />
</button>
</div>
<div className="photoPackSettings-content">
<div className="itemSettings-content">
{hasSettings && (
<>
{validationErrors.length > 0 && (
<div className="photoPackSettings-error">
<div className="itemSettings-error">
<MdWarning />
<span>Configuration incomplete: {validationErrors.join(', ')}</span>
</div>
)}
<div className="photoPackSettings-fields">
<div className="itemSettings-fields">
{pack.settings_schema.map((field) => (
<div key={field.key} className="photoPackSettings-field">
<div key={field.key} className="itemSettings-field">
{renderField(field)}
</div>
))}
</div>
{isPhotoPack && (
<div className="photoPackSettings-actions">
<div className="itemSettings-actions">
<Button
onClick={handleManualRefresh}
icon={<MdRefresh />}
@@ -250,8 +250,8 @@ const ItemSettingsModal = ({ pack, isOpen, onClose, isEnabled }) => {
)}
{!hasSettings && (
<div className="photoPackSettings-info">
<div className="photoPackSettings-info-item">
<div className="itemSettings-info">
<div className="itemSettings-info-item">
<span className="label">Type</span>
<span className="value">
{variables.getMessage(
@@ -260,13 +260,13 @@ const ItemSettingsModal = ({ pack, isOpen, onClose, isEnabled }) => {
</span>
</div>
{pack.description && (
<div className="photoPackSettings-info-item">
<div className="itemSettings-info-item">
<span className="label">Description</span>
<span className="value">{pack.description}</span>
</div>
)}
{pack.sideload && (
<div className="photoPackSettings-info-item">
<div className="itemSettings-info-item">
<span className="label">Source</span>
<span className="value">Sideloaded</span>
</div>

View File

@@ -1,4 +1,4 @@
.photoPackSettingsModal {
.itemSettingsModal {
max-width: 600px;
width: 90%;
max-height: 80vh;
@@ -8,11 +8,11 @@
padding: 0;
}
.photoPackSettingsOverlay {
.itemSettingsOverlay {
background-color: rgba(0, 0, 0, 0.7);
}
.photoPackSettings-header {
.itemSettings-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -20,20 +20,20 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.photoPackSettings-header-info {
.itemSettings-header-info {
display: flex;
align-items: center;
gap: 16px;
}
.photoPackSettings-icon {
.itemSettings-icon {
width: 56px;
height: 56px;
border-radius: 12px;
object-fit: cover;
}
.photoPackSettings-icon-text {
.itemSettings-icon-text {
display: flex;
align-items: center;
justify-content: center;
@@ -43,20 +43,20 @@
font-size: 20px;
}
.photoPackSettings-header-text {
.itemSettings-header-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.photoPackSettings-header-text h2 {
.itemSettings-header-text h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-color, #fff);
}
.photoPackSettings-subtitle {
.itemSettings-subtitle {
font-size: 14px;
color: var(--subtitle-color, rgba(255, 255, 255, 0.6));
display: flex;
@@ -65,7 +65,7 @@
flex-wrap: wrap;
}
.photoPackSettings-version {
.itemSettings-version {
padding: 2px 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
@@ -73,7 +73,7 @@
font-weight: 500;
}
.photoPackSettings-status {
.itemSettings-status {
display: flex;
align-items: center;
gap: 4px;
@@ -97,7 +97,7 @@
}
}
.photoPackSettings-close {
.itemSettings-close {
background: transparent;
border: none;
color: var(--text-color, #fff);
@@ -111,18 +111,18 @@
transition: background 0.2s;
}
.photoPackSettings-close:hover {
.itemSettings-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.photoPackSettings-content {
.itemSettings-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.photoPackSettings-error {
.itemSettings-error {
display: flex;
align-items: center;
gap: 8px;
@@ -134,35 +134,35 @@
font-size: 14px;
}
.photoPackSettings-error svg {
.itemSettings-error svg {
font-size: 20px;
flex-shrink: 0;
}
.photoPackSettings-fields {
.itemSettings-fields {
display: flex;
flex-direction: column;
gap: 16px;
}
.photoPackSettings-field {
.itemSettings-field {
width: 100%;
}
.photoPackSettings-actions {
.itemSettings-actions {
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.photoPackSettings-info {
.itemSettings-info {
display: flex;
flex-direction: column;
gap: 16px;
}
.photoPackSettings-info-item {
.itemSettings-info-item {
display: flex;
flex-direction: column;
gap: 4px;