feat: implement pack enable/disable functionality and improve UI responsiveness

This commit is contained in:
alexsparkes
2026-02-03 15:45:04 +00:00
parent 0acac6dcee
commit bafa2ecbe7
8 changed files with 158 additions and 31 deletions

View File

@@ -114,6 +114,18 @@
} }
} }
// Disabled pack styling
&.item-disabled {
opacity: 0.5;
.card-title,
.card-subtitle {
@include themed {
color: t($subColor);
}
}
}
.item-uninstall-btn { .item-uninstall-btn {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -162,12 +162,17 @@ export async function checkAndRefreshAPIPacks() {
export function buildPhotoPool() { export function buildPhotoPool() {
const pool = []; const pool = [];
const installed = JSON.parse(localStorage.getItem('installed') || '[]'); const installed = JSON.parse(localStorage.getItem('installed') || '[]');
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]'); const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}'); const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
installed.forEach((pack) => { installed.forEach((pack) => {
if (pack.type !== 'photos') return; if (pack.type !== 'photos') return;
// Filter by enabled status - default to enabled if not in enabledPacks object
const packId = pack.id || pack.name;
if (enabledPacks[packId] === false) return;
if (pack.api_enabled) { if (pack.api_enabled) {
// API pack - check if configured and ready // API pack - check if configured and ready
if (apiPacksReady.includes(pack.id)) { if (apiPacksReady.includes(pack.id)) {

View File

@@ -65,7 +65,9 @@ const SourceSection = ({
toggleFunction={onToggle} toggleFunction={onToggle}
showCreateYourOwn={false} showCreateYourOwn={false}
onUninstall={onPhotoPackUninstall} onUninstall={onPhotoPackUninstall}
onTogglePack={() => {}}
viewType="grid" viewType="grid"
showChips={false}
/> />
{/* Settings for API packs */} {/* Settings for API packs */}

View File

@@ -4,7 +4,10 @@ import { MdCheckCircle, MdOutlineUploadFile, MdClose } from 'react-icons/md';
import placeholderIcon from 'assets/icons/marketplace-placeholder.png'; import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
import { Tooltip } from 'components/Elements'; import { Tooltip } from 'components/Elements';
import { Button } from 'components/Elements';
import Switch from 'components/Form/Settings/Switch/Switch';
import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown'; import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown';
import EventBus from 'utils/eventbus';
import { getProxiedImageUrl } from 'utils/marketplace'; import { getProxiedImageUrl } from 'utils/marketplace';
function filterItems(item, filter, categoryFilter) { function filterItems(item, filter, categoryFilter) {
@@ -60,32 +63,92 @@ function ItemCard({
isInstalled, isInstalled,
isAdded, isAdded,
onUninstall, onUninstall,
onTogglePack,
showChips = true,
}) { }) {
item._onCollection = onCollection; item._onCollection = onCollection;
const isSideloaded = item.sideload === true; const isSideloaded = item.sideload === true;
const packId = item.id || item.name;
// Use React state to manage enabled status for immediate UI updates
const [isEnabled, setIsEnabled] = useState(() => {
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
return enabledPacks[packId] !== false; // Default to enabled if not set
});
const handleTogglePack = (e) => {
e.stopPropagation();
const newState = !isEnabled;
// Update local state immediately for UI responsiveness
setIsEnabled(newState);
// Update localStorage
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
enabledPacks[packId] = newState;
localStorage.setItem('enabledPacks', JSON.stringify(enabledPacks));
if (onTogglePack) {
onTogglePack(packId, newState);
}
// Emit refresh event for quotes only (background will update on next interval)
if (item.type === 'quotes') {
EventBus.emit('refresh', 'quote');
}
};
return ( return (
<div <div
className={`item ${isSideloaded ? 'item-sideloaded' : ''}`} className={`item ${isSideloaded ? 'item-sideloaded' : ''} ${!isEnabled && isAdded ? 'item-disabled' : ''}`}
onClick={isSideloaded ? undefined : () => toggleFunction(item)} onClick={isSideloaded ? undefined : () => toggleFunction(item)}
key={item.name} key={item.name}
> >
{isAdded && onUninstall && ( {isAdded && onTogglePack && (
<Tooltip <div
title={variables.getMessage('modals.main.marketplace.product.buttons.remove')} className="item-toggle-switch"
onClick={(e) => e.stopPropagation()}
style={{ position: 'absolute', top: '12px', right: '12px', zIndex: 3 }} style={{ position: 'absolute', top: '12px', right: '12px', zIndex: 3 }}
> >
<button <label className="switch-track" style={{ cursor: 'pointer' }}>
className="item-uninstall-btn" <input
onClick={(e) => { type="checkbox"
e.stopPropagation(); checked={isEnabled}
onUninstall(item.type, item.name); onChange={handleTogglePack}
}} style={{ display: 'none' }}
> />
<MdClose /> <div
</button> className={`switch-track ${isEnabled ? 'checked' : ''}`}
</Tooltip> style={{
width: '52px',
height: '32px',
borderRadius: '16px',
backgroundColor: isEnabled
? 'var(--linkColor, #5298ff)'
: 'rgba(128, 128, 128, 0.3)',
position: 'relative',
transition: 'background-color 0.2s',
cursor: 'pointer',
}}
>
<div
className="switch-thumb"
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: 'white',
position: 'absolute',
top: '4px',
left: isEnabled ? '24px' : '4px',
transition: 'left 0.2s',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
}}
/>
</div>
</label>
</div>
)} )}
{isSideloaded && ( {isSideloaded && (
<Tooltip <Tooltip
@@ -128,18 +191,33 @@ function ItemCard({
'' ''
)} )}
<div className="card-chips"> {showChips && (
{item.type && ( <div className="card-chips">
<span className="card-type"> {item.type && (
{variables.getMessage('modals.main.marketplace.' + getTypeTranslationKey(item.type))} <span className="card-type">
</span> {variables.getMessage('modals.main.marketplace.' + getTypeTranslationKey(item.type))}
)} </span>
{item.in_collections && item.in_collections.length > 0 && !onCollection && ( )}
<span className="card-collection"> {item.in_collections && item.in_collections.length > 0 && !onCollection && (
{item.in_collections[0].display_name || item.in_collections[0].name} <span className="card-collection">
</span> {item.in_collections[0].display_name || item.in_collections[0].name}
)} </span>
</div> )}
</div>
)}
{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> </div>
</div> </div>
); );
@@ -156,7 +234,9 @@ function Items({
onSortChange, onSortChange,
isAdded = false, isAdded = false,
onUninstall, onUninstall,
onTogglePack,
viewType = 'grid', viewType = 'grid',
showChips = true,
}) { }) {
const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all');
const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z'); const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z');
@@ -222,6 +302,8 @@ function Items({
isInstalled={installedNames.has(item.name)} isInstalled={installedNames.has(item.name)}
isAdded={isAdded} isAdded={isAdded}
onUninstall={onUninstall} onUninstall={onUninstall}
onTogglePack={onTogglePack}
showChips={showChips}
key={index} key={index}
/> />
))} ))}

View File

@@ -114,9 +114,7 @@ export function useQuoteLoader(updateQuote) {
} }
// Filter out incomplete quotes (empty quote text) // Filter out incomplete quotes (empty quote text)
const validQuotes = customQuote?.filter( const validQuotes = customQuote?.filter((q) => q.quote && q.quote.trim() !== '') || [];
(q) => q.quote && q.quote.trim() !== ''
) || [];
if (validQuotes.length === 0) { if (validQuotes.length === 0) {
return { noQuote: true }; return { noQuote: true };
@@ -144,8 +142,14 @@ export function useQuoteLoader(updateQuote) {
} }
const installed = JSON.parse(localStorage.getItem('installed') || '[]'); const installed = JSON.parse(localStorage.getItem('installed') || '[]');
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
const quotePack = installed const quotePack = installed
.filter((item) => item.type === 'quotes') .filter((item) => {
if (item.type !== 'quotes') return false;
const packId = item.id || item.name;
// Default to enabled if not in enabledPacks object
return enabledPacks[packId] !== false;
})
.flatMap((item) => .flatMap((item) =>
item.quotes.map((quote) => ({ item.quotes.map((quote) => ({
...quote, ...quote,

View File

@@ -359,7 +359,9 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
<> <>
<Row final={true}> <Row final={true}>
<Content <Content
title={variables.getMessage('modals.main.settings.sections.quote.installed_packs_title')} title={variables.getMessage(
'modals.main.settings.sections.quote.installed_packs_title',
)}
subtitle={`${installedQuotePacks.length} ${installedQuotePacks.length === 1 ? 'pack' : 'packs'}${totalQuotes} ${totalQuotes === 1 ? 'quote' : 'quotes'}`} subtitle={`${installedQuotePacks.length} ${installedQuotePacks.length === 1 ? 'pack' : 'packs'}${totalQuotes} ${totalQuotes === 1 ? 'quote' : 'quotes'}`}
/> />
<Action> <Action>
@@ -378,7 +380,9 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
toggleFunction={handleToggle} toggleFunction={handleToggle}
showCreateYourOwn={false} showCreateYourOwn={false}
onUninstall={handleUninstall} onUninstall={handleUninstall}
onTogglePack={() => {}}
viewType="grid" viewType="grid"
showChips={false}
/> />
</> </>
); );

View File

@@ -129,6 +129,14 @@ export function install(type, input, sideload, collection) {
localStorage.setItem('installed', JSON.stringify(installed)); localStorage.setItem('installed', JSON.stringify(installed));
// Auto-enable pack on install
if (isNewInstall) {
const packId = input.id || input.name;
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
enabledPacks[packId] = true;
localStorage.setItem('enabledPacks', JSON.stringify(enabledPacks));
}
// Track download for new installs (not re-installs) // Track download for new installs (not re-installs)
if (isNewInstall && input.id) { if (isNewInstall && input.id) {
trackDownload(input.id); trackDownload(input.id);

View File

@@ -119,6 +119,16 @@ export function uninstall(type, name) {
localStorage.setItem('installed', JSON.stringify(installed)); localStorage.setItem('installed', JSON.stringify(installed));
// Cleanup enabledPacks entry
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
const installedItems = JSON.parse(localStorage.getItem('installed') || '[]');
const packToRemove = installedItems.find((item) => item.name === name);
if (packToRemove) {
const packId = packToRemove.id || packToRemove.name;
delete enabledPacks[packId];
localStorage.setItem('enabledPacks', JSON.stringify(enabledPacks));
}
// Emit refresh event after all data is saved // Emit refresh event after all data is saved
if (refreshEvent) { if (refreshEvent) {
EventBus.emit('refresh', refreshEvent); EventBus.emit('refresh', refreshEvent);