feat: implement marketplace handler registration and refactor default packs installation

This commit is contained in:
alexsparkes
2026-02-06 22:09:04 +00:00
parent 62f7e20f8e
commit 74565fbeb9
13 changed files with 332 additions and 45 deletions

View File

@@ -8,10 +8,13 @@ import { loadSettings, moveSettings } from 'utils/settings';
import EventBus from 'utils/eventbus';
import variables from 'config/variables';
import { TranslationProvider } from 'contexts/TranslationContext';
import { installDefaultPhotoPacks } from 'utils/marketplace/installDefaultPacks';
import { registerAllHandlers } from 'utils/marketplace/registerHandlers';
import { installDefaultPacks } from 'utils/marketplace/installDefaultPacks';
const useAppSetup = () => {
useEffect(() => {
registerAllHandlers();
const firstRun = localStorage.getItem('firstRun');
const stats = localStorage.getItem('stats');
@@ -22,7 +25,7 @@ const useAppSetup = () => {
loadSettings();
installDefaultPhotoPacks();
installDefaultPacks();
const refreshHandler = (data) => {
if (data === 'other' || data === 'greeting' || data === 'clock' || data === 'quote') {

View File

@@ -0,0 +1,9 @@
{
"photos": [
{ "id": "d58909e759ec", "enabled": true },
{ "id": "c0094361594f", "enabled": false }
],
"quotes": [
{ "id": "0c8a5bdebd13", "enabled": true }
]
}

View File

@@ -293,16 +293,37 @@ export function buildPhotoPool() {
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
console.log('[Build Pool] Building photo pool', {
installed_count: installed.filter((p) => p.type === 'photos').length,
enabledPacks,
apiPacksReady,
});
installed.forEach((pack) => {
if (pack.type !== 'photos') return;
const packId = pack.id || pack.name;
if (enabledPacks[packId] === false) return;
const isEnabled = enabledPacks[packId] !== false;
console.log(`[Build Pool] Processing pack: ${pack.display_name || pack.name}`, {
packId,
enabled: isEnabled,
enabledPacksValue: enabledPacks[packId],
api_enabled: pack.api_enabled,
api_ready: apiPacksReady.includes(pack.id),
cached_photos: pack.api_enabled ? apiPackCache[pack.id]?.photos?.length || 0 : pack.photos?.length || 0,
});
if (enabledPacks[packId] === false) {
console.log(`[Build Pool] Skipping disabled pack: ${pack.display_name || pack.name}`);
return;
}
if (pack.api_enabled) {
if (apiPacksReady.includes(pack.id)) {
const cached = apiPackCache[pack.id];
if (cached && cached.photos.length > 0) {
console.log(`[Build Pool] Adding ${cached.photos.length} API photos from ${pack.display_name || pack.name}`);
cached.photos.forEach((photo) => {
pool.push({
...photo,
@@ -310,9 +331,14 @@ export function buildPhotoPool() {
pack_id: pack.id,
});
});
} else {
console.log(`[Build Pool] API pack ${pack.display_name || pack.name} has no cached photos`);
}
} else {
console.log(`[Build Pool] API pack ${pack.display_name || pack.name} is not ready yet`);
}
} else {
console.log(`[Build Pool] Adding ${pack.photos.length} static photos from ${pack.display_name || pack.name}`);
pack.photos.forEach((photo) => {
pool.push({
photographer: photo.photographer,
@@ -326,5 +352,6 @@ export function buildPhotoPool() {
}
});
console.log(`[Build Pool] Final pool size: ${pool.length} photos`);
return pool;
}

View File

@@ -48,7 +48,8 @@ const formatText = (text) => {
const downloadImage = async (info) => {
const link = document.createElement('a');
link.href = await toDataURL(getProxiedImageUrl(info.url));
link.download = `mue-${formatText(info.credit)}-${formatText(info.location)}.jpg`; // image is more likely to be webp or avif btw
const locationText = typeof info.location === 'string' ? info.location : info.location?.name || 'unknown';
link.download = `mue-${formatText(info.credit)}-${formatText(locationText)}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -341,14 +342,20 @@ function PhotoInformation({ info, url, api }) {
<span
className="title"
title={
(showExtraInfo || other) && info.description ? info.description : info.location
(showExtraInfo || other) && info.description
? info.description
: typeof info.location === 'string'
? info.location
: info.location?.name || ''
}
>
{(showExtraInfo || other) && info.description
? info.description.length > 40
? info.description.substring(0, 40) + '...'
: info.description
: info.location?.split(',').slice(-2).join(', ').trim()}
: typeof info.location === 'string'
? info.location.split(',').slice(-2).join(', ').trim()
: info.location?.name || ''}
</span>
<span className="subtitle" id="credit">
{photo} {credit}

View File

@@ -0,0 +1,105 @@
import { clearQueuesOnSettingChange } from 'utils/queueOperations';
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
export const photoPackHandler = {
install: (packData, context) => {
console.log(`[Photo Handler] Installing ${packData.display_name || packData.name}`, {
api_enabled: packData.api_enabled,
requires_api_key: packData.requires_api_key,
photo_count: packData.photos?.length,
isNewInstall: context.isNewInstall,
});
const currentPhotos = JSON.parse(localStorage.getItem('photo_packs')) || [];
const hadPhotoPacks = currentPhotos.length > 0;
if (packData.api_enabled) {
console.log(`[Photo Handler] Setting up API pack ${packData.id}`);
const defaultSettings = {};
packData.settings_schema?.forEach((field) => {
defaultSettings[field.key] = field.default || '';
});
localStorage.setItem(`photopack_settings_${packData.id}`, JSON.stringify(defaultSettings));
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
apiPackCache[packData.id] = {
photos: [],
last_fetched: 0,
last_refresh_attempt: 0,
};
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
if (!currentPhotos.length) {
localStorage.setItem('photo_packs', JSON.stringify([]));
}
if (!packData.requires_api_key) {
console.log(`[Photo Handler] Refreshing API cache for ${packData.id}`);
refreshAPIPackCache(packData.id);
}
} else {
console.log(`[Photo Handler] Adding ${packData.photos.length} static photos to pool`);
packData.photos.forEach((photo) => {
currentPhotos.push(photo);
});
localStorage.setItem('photo_packs', JSON.stringify(currentPhotos));
console.log(`[Photo Handler] Photo pool now has ${currentPhotos.length} photos`);
}
if (localStorage.getItem('backgroundType') !== 'photo_pack') {
localStorage.setItem('oldBackgroundType', localStorage.getItem('backgroundType'));
}
localStorage.setItem('backgroundType', 'photo_pack');
localStorage.removeItem('backgroundchange');
clearQueuesOnSettingChange('packInstall');
const backgroundElement = document.getElementById('backgroundImage');
const hasBackground = backgroundElement && backgroundElement.style.backgroundImage;
console.log(`[Photo Handler] Background check:`, {
elementExists: !!backgroundElement,
hasBackgroundImage: !!hasBackground,
willRefresh: !hasBackground,
});
return {
refreshEvent: !hasBackground ? 'backgroundrefresh' : null,
};
},
uninstall: (packData, context) => {
let installedContents = JSON.parse(localStorage.getItem('photo_packs')) || [];
if (packData) {
if (packData.api_enabled) {
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
delete apiPackCache[packData.id];
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
const filtered = apiPacksReady.filter((id) => id !== packData.id);
localStorage.setItem('api_packs_ready', JSON.stringify(filtered));
} else if (packData.photos) {
installedContents = installedContents.filter((item) => {
return !packData.photos.some((content) => content.url?.default === item.url?.default);
});
localStorage.setItem('photo_packs', JSON.stringify(installedContents));
}
}
const remainingInstalled = JSON.parse(localStorage.getItem('installed')).filter(
(item) => item.type === 'photos' && item.name !== context.name,
);
if (remainingInstalled.length === 0) {
localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api');
localStorage.removeItem('oldBackgroundType');
localStorage.removeItem('photo_packs');
}
localStorage.removeItem('backgroundchange');
clearQueuesOnSettingChange('packUninstall');
return { refreshEvent: null };
},
};

View File

@@ -0,0 +1,43 @@
export const quotePackHandler = {
install: (packData, context) => {
const currentQuotes = JSON.parse(localStorage.getItem('quote_packs')) || [];
packData.quotes.forEach((quote) => {
currentQuotes.push(quote);
});
localStorage.setItem('quote_packs', JSON.stringify(currentQuotes));
if (localStorage.getItem('quoteType') !== 'quote_pack') {
localStorage.setItem('oldQuoteType', localStorage.getItem('quoteType'));
}
localStorage.setItem('quoteType', 'quote_pack');
localStorage.removeItem('quotechange');
localStorage.removeItem('quoteQueue');
localStorage.removeItem('currentQuote');
return { refreshEvent: 'quote' };
},
uninstall: (packData, context) => {
let installedContents = JSON.parse(localStorage.getItem('quote_packs')) || [];
if (packData && packData.quotes) {
installedContents = installedContents.filter((item) => {
return !packData.quotes.some(
(content) => content.quote === item.quote && content.author === item.author,
);
});
}
localStorage.setItem('quote_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) {
localStorage.setItem('quoteType', localStorage.getItem('oldQuoteType') || 'api');
localStorage.removeItem('oldQuoteType');
localStorage.removeItem('quote_packs');
}
localStorage.removeItem('quotechange');
localStorage.removeItem('quoteQueue');
localStorage.removeItem('currentQuote');
return { refreshEvent: 'marketplacequoteuninstall' };
},
};

View File

@@ -0,0 +1,19 @@
const handlers = new Map();
export function registerHandler(type, handler) {
if (!handler.install || typeof handler.install !== 'function') {
throw new Error(`Handler for type "${type}" must have an install function`);
}
if (!handler.uninstall || typeof handler.uninstall !== 'function') {
throw new Error(`Handler for type "${type}" must have an uninstall function`);
}
handlers.set(type, handler);
}
export function getHandler(type) {
return handlers.get(type) || null;
}
export function hasHandler(type) {
return handlers.has(type);
}

View File

@@ -0,0 +1,34 @@
function showReminder() {
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
}
export const settingsHandler = {
install: (packData, context) => {
localStorage.removeItem('backup_settings');
const oldSettings = [];
Object.keys(localStorage).forEach((key) => {
oldSettings.push({ name: key, value: localStorage.getItem(key) });
});
localStorage.setItem('backup_settings', JSON.stringify(oldSettings));
Object.keys(packData.settings).forEach((key) => {
localStorage.setItem(key, packData.settings[key]);
});
showReminder();
return { refreshEvent: null };
},
uninstall: (packData, context) => {
const oldSettings = JSON.parse(localStorage.getItem('backup_settings'));
localStorage.clear();
oldSettings.forEach((item) => {
localStorage.setItem(item.name, item.value);
});
showReminder();
return { refreshEvent: null };
},
};

View File

@@ -2,5 +2,6 @@ import { install } from './install';
import { uninstall } from './uninstall';
import { urlParser } from './urlParser';
import { getProxiedImageUrl } from './imageProxy';
import { registerHandler, getHandler, hasHandler } from './handlerRegistry';
export { install, uninstall, urlParser, getProxiedImageUrl };
export { install, uninstall, urlParser, getProxiedImageUrl, registerHandler, getHandler, hasHandler };

View File

@@ -2,6 +2,7 @@ import EventBus from 'utils/eventbus';
import { clearQueuesOnSettingChange } from 'utils/queueOperations';
import variables from 'config/variables';
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
import { getHandler } from './handlerRegistry';
// todo: relocate this function
function showReminder() {
@@ -26,12 +27,27 @@ async function trackDownload(itemId) {
}
export function install(type, input, sideload, collection) {
let refreshEvent = null;
console.log(`[Install] Installing ${type}: ${input.display_name || input.name}`, {
sideload,
collection,
id: input.id,
});
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
const isNewInstall = !installed.some((item) => item.id === input.id || item.name === input.name);
switch (type) {
console.log(`[Install] isNewInstall: ${isNewInstall}`);
let refreshEvent = null;
const handler = getHandler(type);
if (handler) {
console.log(`[Install] Using handler for type: ${type}`);
const result = handler.install(input, { isNewInstall, installed });
refreshEvent = result.refreshEvent;
} else {
console.log(`[Install] No handler found, using fallback switch for type: ${type}`);
switch (type) {
case 'settings': {
localStorage.removeItem('backup_settings');
@@ -87,7 +103,10 @@ export function install(type, input, sideload, collection) {
localStorage.setItem('backgroundType', 'photo_pack');
localStorage.removeItem('backgroundchange');
clearQueuesOnSettingChange('packInstall');
if (!hadPhotoPacks) {
const backgroundElement = document.getElementById('backgroundImage');
const hasBackground = backgroundElement && backgroundElement.style.backgroundImage;
if (!hasBackground) {
refreshEvent = 'backgroundrefresh';
}
break;
@@ -113,6 +132,7 @@ export function install(type, input, sideload, collection) {
default:
break;
}
}
if (sideload) {
@@ -122,12 +142,14 @@ export function install(type, input, sideload, collection) {
installed.push(input);
localStorage.setItem('installed', JSON.stringify(installed));
console.log(`[Install] Updated installed list, count: ${installed.length}`);
if (isNewInstall) {
const packId = input.id || input.name;
const enabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
enabledPacks[packId] = true;
localStorage.setItem('enabledPacks', JSON.stringify(enabledPacks));
console.log(`[Install] Set pack ${packId} as enabled`);
}
if (isNewInstall && input.id) {
@@ -135,6 +157,9 @@ export function install(type, input, sideload, collection) {
}
if (refreshEvent) {
console.log(`[Install] Emitting refresh event: ${refreshEvent}`);
EventBus.emit('refresh', refreshEvent);
}
console.log(`[Install] Installation complete for ${input.display_name || input.name}`);
}

View File

@@ -1,64 +1,56 @@
import variables from 'config/variables';
import defaultPacks from 'config/defaultPacks.json';
import { install } from './install';
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
const DEFAULT_PHOTO_PACKS = [
{ id: 'd58909e759ec', enabled: true },
{ id: 'c0094361594f', enabled: false },
];
export async function installDefaultPhotoPacks() {
export async function installDefaultPacks() {
if (localStorage.getItem('defaultPacksNeedInstall') !== 'true') {
console.log('[Default Packs] Already installed, skipping');
return;
}
console.log('[Default Packs] Installing default packs...', defaultPacks);
const installed = [];
const enabledPacks = {};
for (const pack of DEFAULT_PHOTO_PACKS) {
try {
const response = await fetch(`${variables.constants.API_URL}/marketplace/item/${pack.id}`);
const { data } = await response.json();
for (const [type, packs] of Object.entries(defaultPacks)) {
console.log(`[Default Packs] Processing ${type} packs:`, packs);
if (data && data.type === 'photos') {
installed.push(data);
enabledPacks[data.id] = pack.enabled;
for (const pack of packs) {
try {
console.log(`[Default Packs] Fetching pack ${pack.id}, enabled: ${pack.enabled}`);
const response = await fetch(`${variables.constants.API_URL}/marketplace/item/${pack.id}`);
const { data } = await response.json();
if (data.api_enabled) {
const defaultSettings = {};
data.settings_schema?.forEach((field) => {
defaultSettings[field.key] = field.default || '';
});
localStorage.setItem(`photopack_settings_${data.id}`, JSON.stringify(defaultSettings));
if (data && data.type === type) {
console.log(`[Default Packs] Installing ${data.display_name || data.name}`, data);
installed.push(data);
enabledPacks[data.id] = pack.enabled;
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
apiPackCache[data.id] = {
photos: [],
last_fetched: 0,
last_refresh_attempt: 0,
};
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
install(type, data, false, true);
if (pack.enabled && !data.requires_api_key) {
if (type === 'photos' && data.api_enabled && pack.enabled && !data.requires_api_key) {
console.log(`[Default Packs] Refreshing API cache for enabled pack: ${data.id}`);
refreshAPIPackCache(data.id);
}
}
} catch (error) {
console.error(`[Default Packs] Failed to install default ${type} pack ${pack.id}:`, error);
}
} catch (error) {
console.error(`Failed to install default pack ${pack.id}:`, error);
}
}
if (installed.length > 0) {
const existingInstalled = JSON.parse(localStorage.getItem('installed') || '[]');
localStorage.setItem('installed', JSON.stringify([...existingInstalled, ...installed]));
const existingEnabledPacks = JSON.parse(localStorage.getItem('enabledPacks') || '{}');
localStorage.setItem(
'enabledPacks',
JSON.stringify({ ...existingEnabledPacks, ...enabledPacks }),
);
console.log('[Default Packs] Set enabledPacks:', enabledPacks);
localStorage.removeItem('defaultPacksNeedInstall');
window.dispatchEvent(new Event('installedAddonsChanged'));
console.log('[Default Packs] Installation complete');
}
}

View File

@@ -0,0 +1,10 @@
import { registerHandler } from './handlerRegistry';
import { photoPackHandler } from 'features/background/marketplace/photoPackHandler';
import { quotePackHandler } from 'features/quote/marketplace/quotePackHandler';
import { settingsHandler } from './handlers/settingsHandler';
export function registerAllHandlers() {
registerHandler('photos', photoPackHandler);
registerHandler('quotes', quotePackHandler);
registerHandler('settings', settingsHandler);
}

View File

@@ -1,5 +1,6 @@
import EventBus from 'utils/eventbus';
import { clearQueuesOnSettingChange } from 'utils/queueOperations';
import { getHandler } from './handlerRegistry';
// todo: relocate this function
function showReminder() {
@@ -8,10 +9,20 @@ function showReminder() {
}
export function uninstall(type, name) {
let installedContents, packContents;
let refreshEvent = null;
switch (type) {
const handler = getHandler(type);
if (handler) {
const installed = JSON.parse(localStorage.getItem('installed'));
const packData = installed.find((item) => item.name === name);
if (packData) {
const result = handler.uninstall(packData, { name, installed });
refreshEvent = result.refreshEvent;
}
} else {
let installedContents, packContents;
switch (type) {
case 'settings': {
const oldSettings = JSON.parse(localStorage.getItem('backup_settings'));
localStorage.clear();
@@ -83,11 +94,12 @@ export function uninstall(type, name) {
localStorage.removeItem('backgroundchange');
clearQueuesOnSettingChange('packUninstall');
refreshEvent = 'marketplacebackgrounduninstall';
refreshEvent = null;
break;
default:
break;
}
}
const installed = JSON.parse(localStorage.getItem('installed'));