mirror of
https://github.com/mue/mue.git
synced 2026-07-02 12:43:35 +02:00
feat: implement API photo packs with migration and settings management
This commit is contained in:
@@ -7,6 +7,7 @@ import { getAllBackgrounds, getAllBackgroundsWithMetadata } from 'utils/customBa
|
||||
import { BackgroundQueueManager } from 'utils/backgroundQueue';
|
||||
import { getProxiedImageUrl } from 'utils/marketplace';
|
||||
import { safeParseJSON } from 'utils/jsonStorage';
|
||||
import { buildPhotoPool, checkAndRefreshAPIPacks } from './photoPackAPI';
|
||||
|
||||
/**
|
||||
* Fetches image data from the configured API
|
||||
@@ -295,15 +296,15 @@ async function prefetchCustomBackgrounds(queueManager, allBackgrounds, currentId
|
||||
|
||||
/**
|
||||
* Gets photo pack background with prefetching and blurhash
|
||||
* Now supports both static and API-based photo packs
|
||||
*/
|
||||
function getPhotoPackBackground(isOffline) {
|
||||
if (isOffline) return getOfflineImage('photo_pack');
|
||||
|
||||
const photos = safeParseJSON('installed', []).flatMap((item) =>
|
||||
item.type === 'photos' && item.photos ? item.photos : [],
|
||||
);
|
||||
// Build combined pool from static and API packs
|
||||
const pool = buildPhotoPool();
|
||||
|
||||
if (photos.length === 0) return null;
|
||||
if (pool.length === 0) return null;
|
||||
|
||||
const queueManager = new BackgroundQueueManager('photoPackQueue', 3);
|
||||
let photoData;
|
||||
@@ -313,19 +314,20 @@ function getPhotoPackBackground(isOffline) {
|
||||
if (cachedQueue.length > 0) {
|
||||
photoData = queueManager.shift();
|
||||
} else {
|
||||
// Pick random photo
|
||||
const index = Math.floor(Math.random() * photos.length);
|
||||
const selected = photos[index];
|
||||
// Pick random photo from pool
|
||||
const selected = pool[Math.floor(Math.random() * pool.length)];
|
||||
|
||||
photoData = {
|
||||
url: getProxiedImageUrl(selected.url.default),
|
||||
url: getProxiedImageUrl(selected.url.default || selected.url),
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: selected.photographer,
|
||||
location: selected.location,
|
||||
blur_hash: selected.blur_hash || null,
|
||||
url: selected.url.default,
|
||||
url: selected.url.default || selected.url,
|
||||
source: selected.source,
|
||||
pack_id: selected.pack_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -338,7 +340,7 @@ function getPhotoPackBackground(isOffline) {
|
||||
|
||||
// Prefetch more photos in the background
|
||||
if (queueManager.needsPrefetch()) {
|
||||
prefetchPhotoPackImages(queueManager, photos, photoData, cachedQueue).catch((error) => {
|
||||
prefetchPhotoPackImages(queueManager, pool, photoData, cachedQueue).catch((error) => {
|
||||
console.error('Failed to prefetch photo pack images:', error);
|
||||
});
|
||||
}
|
||||
@@ -348,19 +350,23 @@ function getPhotoPackBackground(isOffline) {
|
||||
|
||||
/**
|
||||
* Prefetch photo pack images in the background
|
||||
* Supports both static and API-based photo packs
|
||||
* @param {BackgroundQueueManager} queueManager - The queue manager
|
||||
* @param {Array} allPhotos - All available photos from installed packs
|
||||
* @param {Array} pool - Combined pool of photos from all packs
|
||||
* @param {Object} currentPhoto - The current photo data
|
||||
* @param {Array} currentQueue - The current queue state
|
||||
*/
|
||||
async function prefetchPhotoPackImages(queueManager, allPhotos, currentPhoto, currentQueue) {
|
||||
async function prefetchPhotoPackImages(queueManager, pool, currentPhoto, currentQueue) {
|
||||
const count = queueManager.getSpaceNeeded();
|
||||
|
||||
// Get already used URLs
|
||||
const usedUrls = [currentPhoto.url, ...currentQueue.map((p) => p.url)];
|
||||
|
||||
// Filter available photos
|
||||
const available = allPhotos.filter((p) => !usedUrls.includes(p.url.default));
|
||||
// Filter available photos (handle both URL formats)
|
||||
const available = pool.filter((p) => {
|
||||
const url = p.url.default || p.url;
|
||||
return !usedUrls.includes(url) && !usedUrls.includes(getProxiedImageUrl(url));
|
||||
});
|
||||
|
||||
if (available.length === 0) return;
|
||||
|
||||
@@ -370,16 +376,21 @@ async function prefetchPhotoPackImages(queueManager, allPhotos, currentPhoto, cu
|
||||
|
||||
// Normalize metadata
|
||||
const normalized = selected.map((photo) => ({
|
||||
url: getProxiedImageUrl(photo.url.default),
|
||||
url: getProxiedImageUrl(photo.url.default || photo.url),
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: photo.photographer,
|
||||
location: photo.location,
|
||||
blur_hash: photo.blur_hash || null,
|
||||
url: photo.url.default,
|
||||
url: photo.url.default || photo.url,
|
||||
source: photo.source,
|
||||
pack_id: photo.pack_id,
|
||||
},
|
||||
}));
|
||||
|
||||
queueManager.push(normalized);
|
||||
|
||||
// Check if any API pack cache needs refresh
|
||||
checkAndRefreshAPIPacks();
|
||||
}
|
||||
|
||||
213
src/features/background/api/photoPackAPI.js
Normal file
213
src/features/background/api/photoPackAPI.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import variables from 'config/variables';
|
||||
|
||||
/**
|
||||
* Fetch photos from MUE API based on pack settings
|
||||
* @param {string} packId - The ID of the photo pack
|
||||
* @param {object} settings - Pack-specific settings
|
||||
* @returns {Promise<object|null>} Photo object or null on error
|
||||
*/
|
||||
export async function fetchFromMUE(packId, settings) {
|
||||
const { quality, categories } = settings;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
quality: quality || 'high',
|
||||
categories: (categories || []).join(','),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${variables.constants.API_URL}/images/random?${params}`);
|
||||
|
||||
if (!response.ok) throw new Error(`API error: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
photographer: data.photographer,
|
||||
location: data.location?.name || 'Unknown',
|
||||
url: { default: data.file },
|
||||
blur_hash: data.blur_hash,
|
||||
colour: data.colour,
|
||||
category: data.category,
|
||||
camera: data.camera,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MUE API fetch failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch photos from Unsplash API
|
||||
* @param {string} packId - The ID of the photo pack
|
||||
* @param {object} settings - Pack-specific settings (must include api_key)
|
||||
* @returns {Promise<object|null>} Photo object or null on error
|
||||
*/
|
||||
export async function fetchFromUnsplash(packId, settings) {
|
||||
const { api_key, collections } = settings;
|
||||
|
||||
if (!api_key) {
|
||||
console.warn('Unsplash API key not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Deobfuscate API key
|
||||
const decodedKey = atob(api_key);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
orientation: 'landscape',
|
||||
});
|
||||
|
||||
if (collections) {
|
||||
params.append('collections', collections);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.unsplash.com/photos/random?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Client-ID ${decodedKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
console.warn('Unsplash rate limit hit');
|
||||
}
|
||||
throw new Error(`Unsplash API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
photographer: data.user.name,
|
||||
location: data.location?.title || 'Unknown',
|
||||
url: { default: data.urls.regular },
|
||||
blur_hash: data.blur_hash,
|
||||
colour: data.color,
|
||||
unsplash_url: data.links.html,
|
||||
photographer_url: data.user.links.html,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Unsplash API fetch failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple photos for a pack and update cache
|
||||
* @param {string} packId - The ID of the photo pack to refresh
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
export async function refreshAPIPackCache(packId) {
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
const pack = installed.find((p) => p.id === packId);
|
||||
|
||||
if (!pack || !pack.api_enabled) return false;
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem(`photopack_settings_${packId}`) || '{}');
|
||||
|
||||
const fetchFunction = pack.api_provider === 'mue' ? fetchFromMUE : fetchFromUnsplash;
|
||||
|
||||
// Fetch 8 photos
|
||||
const promises = Array.from({ length: 8 }, () => fetchFunction(packId, settings));
|
||||
const results = await Promise.all(promises);
|
||||
const validPhotos = results.filter(Boolean);
|
||||
|
||||
if (validPhotos.length === 0) {
|
||||
console.warn(`No photos fetched for pack ${packId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update cache
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
apiPackCache[packId] = {
|
||||
photos: validPhotos,
|
||||
last_fetched: Date.now(),
|
||||
last_refresh_attempt: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
// Keep only 5 most recent photos
|
||||
apiPackCache[packId].photos = validPhotos.slice(0, 5);
|
||||
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all API packs and refresh if needed
|
||||
* Hardcoded 1 hour refresh interval (separate from global background change frequency)
|
||||
*/
|
||||
export async function checkAndRefreshAPIPacks() {
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
|
||||
// Hardcoded 1 hour refresh interval (separate from global background change frequency)
|
||||
const CACHE_REFRESH_INTERVAL = 3600 * 1000; // 1 hour in milliseconds
|
||||
|
||||
for (const packId of apiPacksReady) {
|
||||
const cached = apiPackCache[packId];
|
||||
|
||||
const needsRefresh =
|
||||
!cached ||
|
||||
Date.now() - cached.last_fetched > CACHE_REFRESH_INTERVAL ||
|
||||
cached.photos.length < 3;
|
||||
|
||||
if (needsRefresh) {
|
||||
// Don't block - refresh in background
|
||||
refreshAPIPackCache(packId).catch((error) => {
|
||||
console.error(`Failed to refresh ${packId}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build combined photo pool from static and API packs
|
||||
* @returns {Array} Combined array of photos from all sources
|
||||
*/
|
||||
export function buildPhotoPool() {
|
||||
const pool = [];
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
|
||||
installed.forEach((pack) => {
|
||||
if (pack.type !== 'photos') return;
|
||||
|
||||
if (pack.api_enabled) {
|
||||
// API pack - check if configured and ready
|
||||
if (apiPacksReady.includes(pack.id)) {
|
||||
const cached = apiPackCache[pack.id];
|
||||
if (cached && cached.photos.length > 0) {
|
||||
// Add cached photos with source metadata
|
||||
cached.photos.forEach((photo) => {
|
||||
pool.push({
|
||||
...photo,
|
||||
source: `api:${pack.api_provider}`,
|
||||
pack_id: pack.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Static pack - add all photos
|
||||
pack.photos.forEach((photo) => {
|
||||
pool.push({
|
||||
photographer: photo.photographer,
|
||||
location: photo.location,
|
||||
url: photo.url,
|
||||
blur_hash: photo.blur_hash,
|
||||
source: 'static',
|
||||
pack_id: pack.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return pool;
|
||||
}
|
||||
241
src/features/background/options/sections/PhotoPackSettings.jsx
Normal file
241
src/features/background/options/sections/PhotoPackSettings.jsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import variables from 'config/variables';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { Dropdown, Text, Switch, Slider } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
import { Button } from 'components/Elements';
|
||||
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
|
||||
import { MdRefresh, MdWarning } from 'react-icons/md';
|
||||
|
||||
/**
|
||||
* ChipSelect component for multi-select options
|
||||
*/
|
||||
const ChipSelect = ({ label, options, defaultValue, name, onChange }) => {
|
||||
const [selected, setSelected] = useState(defaultValue || []);
|
||||
|
||||
const toggleChip = (value) => {
|
||||
const newSelected = selected.includes(value)
|
||||
? selected.filter((v) => v !== value)
|
||||
: [...selected, value];
|
||||
setSelected(newSelected);
|
||||
onChange(newSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ fontSize: '14px', fontWeight: 500 }}>{label}</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => toggleChip(option.value)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid',
|
||||
borderColor: selected.includes(option.value) ? '#4CAF50' : '#ccc',
|
||||
backgroundColor: selected.includes(option.value) ? '#4CAF50' : 'transparent',
|
||||
color: selected.includes(option.value) ? '#fff' : '#333',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PhotoPackSettings = ({ pack }) => {
|
||||
if (!pack.settings_schema || pack.settings_schema.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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([]);
|
||||
|
||||
// Load dynamic options (e.g., categories from API)
|
||||
useEffect(() => {
|
||||
pack.settings_schema.forEach((field) => {
|
||||
if (field.dynamic && field.options_source) {
|
||||
loadDynamicOptions(field);
|
||||
}
|
||||
});
|
||||
}, [pack.id]);
|
||||
|
||||
// Validate settings
|
||||
useEffect(() => {
|
||||
validateSettings();
|
||||
}, [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.map((cat) => ({ value: cat, label: cat })),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateSettings = () => {
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
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, index) => {
|
||||
const value =
|
||||
field.secure && settings[field.key] ? atob(settings[field.key]) : settings[field.key] || field.default;
|
||||
|
||||
switch (field.type) {
|
||||
case 'dropdown':
|
||||
return (
|
||||
<Dropdown
|
||||
label={field.label}
|
||||
name={`${pack.id}_${field.key}`}
|
||||
value={value}
|
||||
items={field.options}
|
||||
onChange={(newValue) => handleSettingChange(field.key, newValue)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'chipselect':
|
||||
const options = field.dynamic ? dynamicOptions[field.key] || [] : field.options;
|
||||
return (
|
||||
<ChipSelect
|
||||
label={field.label}
|
||||
options={options}
|
||||
defaultValue={value}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Content
|
||||
title={`${pack.name} Settings`}
|
||||
subtitle={pack.api_provider === 'mue' ? 'MUE API' : 'Unsplash API'}
|
||||
/>
|
||||
<Action>
|
||||
<Button
|
||||
onClick={handleManualRefresh}
|
||||
icon={<MdRefresh />}
|
||||
label="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, index)}</Action>
|
||||
</Row>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhotoPackSettings;
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -59,6 +60,11 @@ const SourceSection = ({
|
||||
onUninstall={onPhotoPackUninstall}
|
||||
viewType="grid"
|
||||
/>
|
||||
|
||||
{/* Settings for API packs */}
|
||||
{installedPhotoPacks.map((pack) =>
|
||||
pack.api_enabled ? <PhotoPackSettings key={pack.id} pack={pack} /> : null,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as Sentry from '@sentry/react';
|
||||
import App from './App';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import variables from './config/variables';
|
||||
import { migrateAPIUsersToPhotoPacks } from './utils/migrations';
|
||||
|
||||
import './scss/index.scss';
|
||||
// the toast css is based on default so we need to import it
|
||||
@@ -20,6 +21,9 @@ document.documentElement.lang = languagecode.replace('_', '-');
|
||||
variables.getMessage = (text, optional) =>
|
||||
variables.language.getMessage(variables.languagecode, text, optional || {});
|
||||
|
||||
// Migrate existing API users to photo packs (one-time migration)
|
||||
migrateAPIUsersToPhotoPacks();
|
||||
|
||||
Sentry.init({
|
||||
dsn: variables.constants.SENTRY_DSN,
|
||||
defaultIntegrations: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { clearQueuesOnSettingChange } from 'utils/queueOperations';
|
||||
import variables from 'config/variables';
|
||||
import { refreshAPIPackCache } from 'features/background/api/photoPackAPI';
|
||||
|
||||
// todo: relocate this function
|
||||
function showReminder() {
|
||||
@@ -51,10 +52,36 @@ export function install(type, input, sideload, collection) {
|
||||
case 'photos': {
|
||||
const currentPhotos = JSON.parse(localStorage.getItem('photo_packs')) || [];
|
||||
const hadPhotoPacks = currentPhotos.length > 0;
|
||||
input.photos.forEach((photo) => {
|
||||
currentPhotos.push(photo);
|
||||
});
|
||||
localStorage.setItem('photo_packs', JSON.stringify(currentPhotos));
|
||||
|
||||
// Handle API packs differently
|
||||
if (input.api_enabled) {
|
||||
// Initialize default settings
|
||||
const defaultSettings = {};
|
||||
input.settings_schema?.forEach((field) => {
|
||||
defaultSettings[field.key] = field.default || '';
|
||||
});
|
||||
localStorage.setItem(`photopack_settings_${input.id}`, JSON.stringify(defaultSettings));
|
||||
|
||||
// Initialize empty cache
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
apiPackCache[input.id] = {
|
||||
photos: [],
|
||||
last_fetched: 0,
|
||||
last_refresh_attempt: 0,
|
||||
};
|
||||
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
|
||||
|
||||
// If no API key required, fetch initial photos
|
||||
if (!input.requires_api_key) {
|
||||
refreshAPIPackCache(input.id);
|
||||
}
|
||||
} else {
|
||||
// Static pack - add photos to pool
|
||||
input.photos.forEach((photo) => {
|
||||
currentPhotos.push(photo);
|
||||
});
|
||||
localStorage.setItem('photo_packs', JSON.stringify(currentPhotos));
|
||||
}
|
||||
|
||||
if (localStorage.getItem('backgroundType') !== 'photo_pack') {
|
||||
localStorage.setItem('oldBackgroundType', localStorage.getItem('backgroundType'));
|
||||
|
||||
@@ -52,18 +52,44 @@ export function uninstall(type, name) {
|
||||
packContents = JSON.parse(localStorage.getItem('installed')).find(
|
||||
(content) => content.name === name,
|
||||
);
|
||||
if (packContents && packContents.photos) {
|
||||
installedContents = installedContents.filter((item) => {
|
||||
return !packContents.photos.some((content) => content.url?.default === item.url?.default);
|
||||
});
|
||||
|
||||
if (packContents) {
|
||||
if (packContents.api_enabled) {
|
||||
// Remove API pack cache
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
delete apiPackCache[packContents.id];
|
||||
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
|
||||
|
||||
// Remove from ready list
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
const filtered = apiPacksReady.filter((id) => id !== packContents.id);
|
||||
localStorage.setItem('api_packs_ready', JSON.stringify(filtered));
|
||||
|
||||
// Keep settings for easy reinstall (optional - can remove if desired)
|
||||
// localStorage.removeItem(`photopack_settings_${packContents.id}`);
|
||||
} else if (packContents.photos) {
|
||||
// Remove static photos
|
||||
installedContents = installedContents.filter((item) => {
|
||||
return !packContents.photos.some(
|
||||
(content) => content.url?.default === item.url?.default,
|
||||
);
|
||||
});
|
||||
localStorage.setItem('photo_packs', JSON.stringify(installedContents));
|
||||
}
|
||||
}
|
||||
localStorage.setItem('photo_packs', JSON.stringify(installedContents));
|
||||
if (installedContents.length === 0) {
|
||||
|
||||
// Check if all packs are uninstalled
|
||||
const remainingInstalled = JSON.parse(localStorage.getItem('installed')).filter(
|
||||
(item) => item.type === 'photos' && item.name !== name,
|
||||
);
|
||||
|
||||
if (remainingInstalled.length === 0) {
|
||||
// Switch back to old background type or default to mue api
|
||||
localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api');
|
||||
localStorage.removeItem('oldBackgroundType');
|
||||
localStorage.removeItem('photo_packs');
|
||||
}
|
||||
|
||||
localStorage.removeItem('backgroundchange');
|
||||
// Clear image queue to ensure fresh background loads
|
||||
clearQueuesOnSettingChange('packUninstall');
|
||||
|
||||
170
src/utils/migrations.js
Normal file
170
src/utils/migrations.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Migrate existing API users to API photo packs
|
||||
* Run once on extension load after update
|
||||
*/
|
||||
export function migrateAPIUsersToPhotoPacks() {
|
||||
// Check if migration already completed
|
||||
if (localStorage.getItem('api_migration_completed') === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundType = localStorage.getItem('backgroundType');
|
||||
const backgroundAPI = localStorage.getItem('backgroundAPI');
|
||||
|
||||
// Only migrate if user is currently using API backgrounds
|
||||
if (backgroundType !== 'api') {
|
||||
localStorage.setItem('api_migration_completed', 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
let packToInstall = null;
|
||||
|
||||
// Determine which API pack to install
|
||||
if (backgroundAPI === 'mue') {
|
||||
packToInstall = {
|
||||
id: 'mue-photos',
|
||||
name: 'MUE Daily Photos',
|
||||
type: 'photos',
|
||||
api_enabled: true,
|
||||
api_provider: 'mue',
|
||||
requires_api_key: false,
|
||||
photos: [],
|
||||
settings_schema: [
|
||||
{
|
||||
key: 'quality',
|
||||
type: 'dropdown',
|
||||
label: 'Image Quality',
|
||||
default: 'high',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'low', label: 'Low (Faster)' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High (Best Quality)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
type: 'chipselect',
|
||||
label: 'Categories',
|
||||
default: ['nature'],
|
||||
required: true,
|
||||
dynamic: true,
|
||||
options_source: 'api:categories',
|
||||
},
|
||||
],
|
||||
version: '1.0.0',
|
||||
author: 'MUE Team',
|
||||
description: 'Fresh photos from MUE API',
|
||||
icon_url: 'https://raw.githubusercontent.com/mue/branding/main/logo/logo_square.png',
|
||||
};
|
||||
|
||||
// Port existing settings
|
||||
const existingQuality = localStorage.getItem('apiQuality') || 'high';
|
||||
const existingCategories = JSON.parse(localStorage.getItem('apiCategories') || '["nature"]');
|
||||
|
||||
const migratedSettings = {
|
||||
quality: existingQuality,
|
||||
categories: existingCategories,
|
||||
};
|
||||
|
||||
localStorage.setItem('photopack_settings_mue-photos', JSON.stringify(migratedSettings));
|
||||
} else if (backgroundAPI === 'unsplash') {
|
||||
packToInstall = {
|
||||
id: 'unsplash-photos',
|
||||
name: 'Unsplash Photos',
|
||||
type: 'photos',
|
||||
api_enabled: true,
|
||||
api_provider: 'unsplash',
|
||||
requires_api_key: true,
|
||||
photos: [],
|
||||
settings_schema: [
|
||||
{
|
||||
key: 'api_key',
|
||||
type: 'text',
|
||||
label: 'Unsplash Access Key',
|
||||
placeholder: 'Enter your Unsplash API key',
|
||||
default: '',
|
||||
required: true,
|
||||
secure: true,
|
||||
help_text: 'Get your free API key at https://unsplash.com/developers',
|
||||
},
|
||||
{
|
||||
key: 'collections',
|
||||
type: 'text',
|
||||
label: 'Collection IDs',
|
||||
placeholder: 'e.g. 123456, 654321',
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
version: '1.0.0',
|
||||
author: 'MUE Team',
|
||||
description: 'Photos from Unsplash collections',
|
||||
icon_url: 'https://raw.githubusercontent.com/mue/branding/main/logo/logo_square.png',
|
||||
};
|
||||
|
||||
// Port existing Unsplash settings (collection IDs only, not API key since it's server-side)
|
||||
const existingCollections = localStorage.getItem('unsplashCollections') || '';
|
||||
|
||||
const migratedSettings = {
|
||||
collections: existingCollections,
|
||||
api_key: '', // User will need to provide their own API key
|
||||
};
|
||||
|
||||
localStorage.setItem('photopack_settings_unsplash-photos', JSON.stringify(migratedSettings));
|
||||
|
||||
// Note: Unsplash users will need to configure their API key after migration
|
||||
console.log('Unsplash migration: Please configure your API key in photo pack settings');
|
||||
}
|
||||
|
||||
if (packToInstall) {
|
||||
// Install the pack
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
|
||||
// Check if not already installed
|
||||
if (!installed.some((item) => item.id === packToInstall.id)) {
|
||||
installed.push(packToInstall);
|
||||
localStorage.setItem('installed', JSON.stringify(installed));
|
||||
}
|
||||
|
||||
// Initialize cache
|
||||
const apiPackCache = JSON.parse(localStorage.getItem('api_pack_cache') || '{}');
|
||||
if (!apiPackCache[packToInstall.id]) {
|
||||
apiPackCache[packToInstall.id] = {
|
||||
photos: [],
|
||||
last_fetched: 0,
|
||||
last_refresh_attempt: 0,
|
||||
};
|
||||
localStorage.setItem('api_pack_cache', JSON.stringify(apiPackCache));
|
||||
}
|
||||
|
||||
// Add to ready list if MUE (no API key required)
|
||||
if (packToInstall.api_provider === 'mue') {
|
||||
const apiPacksReady = JSON.parse(localStorage.getItem('api_packs_ready') || '[]');
|
||||
if (!apiPacksReady.includes(packToInstall.id)) {
|
||||
apiPacksReady.push(packToInstall.id);
|
||||
localStorage.setItem('api_packs_ready', JSON.stringify(apiPacksReady));
|
||||
}
|
||||
}
|
||||
|
||||
// Change background type to photo_pack
|
||||
localStorage.setItem('backgroundType', 'photo_pack');
|
||||
|
||||
// Clear old queue
|
||||
localStorage.removeItem('imageQueue');
|
||||
|
||||
// Fetch initial photos for MUE
|
||||
if (packToInstall.api_provider === 'mue') {
|
||||
import('../features/background/api/photoPackAPI').then((module) => {
|
||||
module.refreshAPIPackCache(packToInstall.id);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Migrated from API background (${backgroundAPI}) to ${packToInstall.name} photo pack`,
|
||||
);
|
||||
}
|
||||
|
||||
// Mark migration as completed
|
||||
localStorage.setItem('api_migration_completed', 'true');
|
||||
}
|
||||
Reference in New Issue
Block a user