Add new localization strings for quote packs and update caching logic for authors

- Added "Installed Quote Packs" and "Get More" strings to multiple language files.
- Updated the author caching mechanism in wikidataAuthorFetcher.js to limit the number of cached entries to 50 and prune old entries when the limit is exceeded.
- Enhanced the occupation extraction logic to return the highest-ranked occupation based on Wikidata rank.
This commit is contained in:
alexsparkes
2026-02-01 20:51:52 +00:00
parent 8356ea1af5
commit 9f461edb55
38 changed files with 462 additions and 59 deletions

View File

@@ -1,8 +1,11 @@
import variables from 'config/variables';
import { memo, useState, useEffect, useCallback, useRef } from 'react';
import { MdSource, MdOutlineAutoAwesome } from 'react-icons/md';
import { toast } from 'react-toastify';
import EventBus from 'utils/eventbus';
import { clearQueuesOnSettingChange } from 'utils/queueOperations';
import { uninstall } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking';
import { Header } from 'components/Layout/Settings';
import { Dropdown } from 'components/Form/Settings';
@@ -33,6 +36,18 @@ const BackgroundOptions = memo(({ currentSubSection, onSubSectionChange, section
);
const [marketplaceEnabled] = useState(localStorage.getItem('photo_packs'));
// Helper function to get installed photo packs
const getInstalledPhotoPacks = () => {
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
return installed.filter((item) => item.type === 'photos');
} catch {
return [];
}
};
const [installedPhotoPacks, setInstalledPhotoPacks] = useState(getInstalledPhotoPacks());
// Auto-show source section for types without effects/display settings
const shouldShowSourceByDefault = ['colour', 'random_colour', 'random_gradient'].includes(
backgroundType,
@@ -106,6 +121,56 @@ const BackgroundOptions = memo(({ currentSubSection, onSubSectionChange, section
};
}, [getBackgroundCategories]);
// Listen for installed addons changes
useEffect(() => {
const handleInstalledAddonsChanged = () => {
setInstalledPhotoPacks(getInstalledPhotoPacks());
// Update backgroundType if it changed (e.g., when all packs are uninstalled)
const currentType = localStorage.getItem('backgroundType') || 'api';
if (currentType !== backgroundType) {
setBackgroundType(currentType);
}
};
window.addEventListener('installedAddonsChanged', handleInstalledAddonsChanged);
return () => window.removeEventListener('installedAddonsChanged', handleInstalledAddonsChanged);
}, [backgroundType]);
// Handle photo pack uninstall
const handlePhotoPackUninstall = (type, name) => {
uninstall(type, name);
toast(variables.getMessage('toasts.uninstalled'));
variables.stats.postEvent('marketplace-item', `${name} uninstalled`);
setInstalledPhotoPacks(getInstalledPhotoPacks());
window.dispatchEvent(new window.Event('installedAddonsChanged'));
};
// Navigate to photo packs marketplace
const goToPhotoPacks = () => {
variables.updateHash('#discover/photo_packs');
const event = new window.Event('popstate');
window.dispatchEvent(event);
};
const handleToggle = (pack) => {
// Navigate to discover tab with the item
const itemId = pack.name;
updateHash(`#discover/all?item=${itemId}`);
// Trigger navigation
const event = new window.Event('popstate');
window.dispatchEvent(event);
variables.stats.postEvent('marketplace', 'ItemPage viewed');
};
// Get total count of photos across all installed packs
const getTotalPhotoCount = () => {
return installedPhotoPacks.reduce((total, pack) => {
return total + (pack.photos?.length || 0);
}, 0);
};
const getBackgroundSettings = () => {
switch (backgroundType) {
case 'custom':
@@ -231,11 +296,16 @@ const BackgroundOptions = memo(({ currentSubSection, onSubSectionChange, section
<SourceSection
backgroundType={backgroundType}
marketplaceEnabled={marketplaceEnabled}
installedPhotoPacks={installedPhotoPacks}
totalPhotoCount={getTotalPhotoCount()}
onTypeChange={(value) => {
// Clear prefetch queue when changing background type
clearQueuesOnSettingChange('backgroundType');
setBackgroundType(value);
}}
onPhotoPackUninstall={handlePhotoPackUninstall}
onGoToPhotoPacks={goToPhotoPacks}
onToggle={handleToggle}
/>
{getBackgroundSettings()}
</>

View File

@@ -1,25 +1,67 @@
import variables from 'config/variables';
import { MdExplore } from 'react-icons/md';
import { Dropdown } from 'components/Form/Settings';
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';
const SourceSection = ({ backgroundType, marketplaceEnabled, onTypeChange }) => {
const SourceSection = ({
backgroundType,
marketplaceEnabled,
installedPhotoPacks = [],
totalPhotoCount = 0,
onTypeChange,
onPhotoPackUninstall,
onGoToPhotoPacks,
onToggle,
}) => {
const showInstalledPhotoPacks =
backgroundType === 'photo_pack' &&
marketplaceEnabled &&
installedPhotoPacks.length > 0;
return (
<Row final={backgroundType === 'random_colour' || backgroundType === 'random_gradient'}>
<Content
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
subtitle={variables.getMessage('modals.main.settings.sections.background.source.subtitle')}
/>
<Action>
<Dropdown
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
name="backgroundType"
onChange={onTypeChange}
category="background"
items={getBackgroundOptionItems(marketplaceEnabled)}
<>
<Row final={!showInstalledPhotoPacks && (backgroundType === 'random_colour' || backgroundType === 'random_gradient')}>
<Content
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
subtitle={variables.getMessage('modals.main.settings.sections.background.source.subtitle')}
/>
</Action>
</Row>
<Action>
<Dropdown
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
name="backgroundType"
onChange={onTypeChange}
category="background"
items={getBackgroundOptionItems(marketplaceEnabled)}
/>
</Action>
</Row>
{showInstalledPhotoPacks && (
<>
<Row final={true}>
<Content
title={variables.getMessage('modals.main.settings.sections.background.installed_packs_title')}
subtitle={`${installedPhotoPacks.length} ${installedPhotoPacks.length === 1 ? 'pack' : 'packs'}${totalPhotoCount} ${totalPhotoCount === 1 ? 'photo' : 'photos'}`}
/>
<Action>
<Button onClick={onGoToPhotoPacks} icon={<MdExplore />} label="Get more" />
</Action>
</Row>
<Items
items={installedPhotoPacks}
isAdded={true}
filter=""
toggleFunction={onToggle}
showCreateYourOwn={false}
onUninstall={onPhotoPackUninstall}
viewType="grid"
/>
</>
)}
</>
);
};

View File

@@ -113,14 +113,19 @@ export function useQuoteLoader(updateQuote) {
];
}
if (!customQuote || customQuote.length === 0) {
// Filter out incomplete quotes (empty quote text)
const validQuotes = customQuote?.filter(
(q) => q.quote && q.quote.trim() !== ''
) || [];
if (validQuotes.length === 0) {
return { noQuote: true };
}
const selected = customQuote[Math.floor(Math.random() * customQuote.length)];
const selected = validQuotes[Math.floor(Math.random() * validQuotes.length)];
return {
quote: `"${selected.quote}"`,
author: selected.author,
author: selected.author || 'Unknown',
authorlink: getAuthorLink(selected.author),
needsAuthorData: true,
noQuote: false,

View File

@@ -1,6 +1,7 @@
import variables from 'config/variables';
import React, { useState, useEffect } from 'react';
import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote } from 'react-icons/md';
import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote, MdExplore } from 'react-icons/md';
import { toast } from 'react-toastify';
import {
Header,
@@ -13,6 +14,9 @@ import {
import { Checkbox, Dropdown, Textarea } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import { FREQUENCY_OPTIONS } from 'utils/frequencyManager';
import Items from 'features/marketplace/components/Items/Items';
import { uninstall } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking';
import './QuoteOptions.scss';
const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => {
@@ -192,6 +196,78 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
const isSourceSection = currentSubSection === 'source';
// Get installed quote packs
const getInstalledQuotePacks = () => {
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
return installed.filter((item) => item.type === 'quotes');
} catch (e) {
return [];
}
};
const [installedQuotePacks, setInstalledQuotePacks] = useState(getInstalledQuotePacks());
// Listen for changes to installed addons
useEffect(() => {
const handleInstalledAddonsChanged = () => {
setInstalledQuotePacks(getInstalledQuotePacks());
// Update quoteType if it changed (e.g., when all packs are uninstalled)
const currentType = localStorage.getItem('quoteType') || 'api';
if (currentType !== quoteType) {
setQuoteType(currentType);
}
};
window.addEventListener('installedAddonsChanged', handleInstalledAddonsChanged);
return () => {
window.removeEventListener('installedAddonsChanged', handleInstalledAddonsChanged);
};
}, [quoteType]);
const handleUninstall = (type, name) => {
// Prevent removing the default pack if it's the only one remaining
const DEFAULT_PACK_ID = '0c8a5bdebd13';
if (installedQuotePacks.length === 1) {
const remainingPack = installedQuotePacks[0];
if (remainingPack.id === DEFAULT_PACK_ID || remainingPack.name === name) {
toast(variables.getMessage('toasts.quote_pack_only_one'));
return;
}
}
uninstall(type, name);
toast(variables.getMessage('toasts.uninstalled'));
variables.stats.postEvent('marketplace-item', `${name} uninstalled`);
variables.stats.postEvent('marketplace', 'Uninstall');
setInstalledQuotePacks(getInstalledQuotePacks());
window.dispatchEvent(new window.Event('installedAddonsChanged'));
};
const goToQuotePacks = () => {
updateHash('#discover/quote_packs');
const event = new window.Event('popstate');
window.dispatchEvent(event);
};
const handleToggle = (pack) => {
// Navigate to discover tab with the item
const itemId = pack.name;
updateHash(`#discover/all?item=${itemId}`);
// Trigger navigation
const event = new window.Event('popstate');
window.dispatchEvent(event);
variables.stats.postEvent('marketplace', 'ItemPage viewed');
};
const getTotalQuoteCount = () => {
return installedQuotePacks.reduce((total, pack) => {
return total + (pack.quotes?.length || 0);
}, 0);
};
let customSettings;
if (quoteType === 'custom' && isSourceSection) {
customSettings = (
@@ -277,6 +353,35 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
)}
</>
);
} else if (quoteType === 'quote_pack' && isSourceSection && installedQuotePacks.length > 0) {
const totalQuotes = getTotalQuoteCount();
customSettings = (
<>
<Row final={true}>
<Content
title={variables.getMessage('modals.main.settings.sections.quote.installed_packs_title')}
subtitle={`${installedQuotePacks.length} ${installedQuotePacks.length === 1 ? 'pack' : 'packs'}${totalQuotes} ${totalQuotes === 1 ? 'quote' : 'quotes'}`}
/>
<Action>
<Button
type="settings"
onClick={goToQuotePacks}
icon={<MdExplore />}
label={variables.getMessage('modals.main.settings.sections.quote.get_more')}
/>
</Action>
</Row>
<Items
items={installedQuotePacks}
isAdded={true}
filter=""
toggleFunction={handleToggle}
showCreateYourOwn={false}
onUninstall={handleUninstall}
viewType="grid"
/>
</>
);
} else {
// api
customSettings = <></>;