chore(config form): update styling and text in config form

This commit is contained in:
Tom Wheeler
2025-09-12 02:32:12 +12:00
parent 4cbd2516ab
commit f7691f9a84
15 changed files with 251 additions and 135 deletions
@@ -1591,3 +1591,88 @@ export async function findPlexItemsByTmdbIds(
return results;
}
// Multi-source collection sync counter utilities
/**
* Get the current sync counter for a multi-source collection
*/
export function getCollectionSyncCounter(configId: string): number {
try {
const settings = getSettings();
const collectionConfigs = settings.plex.collectionConfigs || [];
const config = collectionConfigs.find((c) => c.id === configId);
if (!config) {
logger.warn(`Config not found for sync counter: ${configId}`, {
label: 'Collection Utilities',
configId,
});
return 0;
}
// Get sync counter from config (with type assertion for extended properties)
const extendedConfig = config as typeof config & { syncCounter?: number };
return extendedConfig.syncCounter || 0;
} catch (error) {
logger.error(`Failed to get sync counter for ${configId}: ${error}`, {
label: 'Collection Utilities',
configId,
error: error instanceof Error ? error.message : String(error),
});
return 0;
}
}
/**
* Increment and persist the sync counter for a multi-source collection
*/
export function incrementCollectionSyncCounter(configId: string): number {
try {
const settings = getSettings();
const collectionConfigs = settings.plex.collectionConfigs || [];
const configIndex = collectionConfigs.findIndex((c) => c.id === configId);
if (configIndex < 0) {
logger.warn(`Config not found for sync counter increment: ${configId}`, {
label: 'Collection Utilities',
configId,
});
return 0;
}
const existingConfig = collectionConfigs[configIndex];
// Get current counter and increment (with type assertion for extended properties)
const extendedConfig = existingConfig as typeof existingConfig & {
syncCounter?: number;
};
const newCounter = (extendedConfig.syncCounter || 0) + 1;
// Update config with new counter
const updatedConfig = {
...existingConfig,
syncCounter: newCounter,
};
// Save updated config
collectionConfigs[configIndex] = updatedConfig;
settings.plex.collectionConfigs = collectionConfigs;
settings.save();
logger.debug(`Incremented sync counter for ${configId}: ${newCounter}`, {
label: 'Collection Utilities',
configId,
syncCounter: newCounter,
});
return newCounter;
} catch (error) {
logger.error(`Failed to increment sync counter for ${configId}: ${error}`, {
label: 'Collection Utilities',
configId,
error: error instanceof Error ? error.message : String(error),
});
return 0;
}
}
@@ -15,8 +15,10 @@ import type { BaseCollectionSync } from '@server/lib/collections/core/BaseCollec
import {
createCollectionLabel,
createSyncError,
getCollectionSyncCounter,
getMediaTypeFromLibrary,
handleRateLimit,
incrementCollectionSyncCounter,
parseConfigIdFromLabel,
updateConfigWithRatingKey,
validateAndSanitizeItems,
@@ -131,6 +133,20 @@ export class MultiSourceOrchestrator {
isActive: timeRestrictionResult.isActive,
});
// Increment sync counter for cycle_lists mode
if (config.combineMode === 'cycle_lists') {
const newCounter = incrementCollectionSyncCounter(config.id);
logger.debug(
`Incremented sync counter for cycle collection: ${config.name}`,
{
label: 'Multi-Source Orchestrator',
configId: config.id,
syncCounter: newCounter,
}
);
}
const itemGroups: CollectionItem[][] = [];
// Fetch items from each source
@@ -176,7 +192,11 @@ export class MultiSourceOrchestrator {
}
// Combine items according to mode
const combinedItems = this.combineItems(itemGroups, config.combineMode);
const combinedItems = this.combineItems(
itemGroups,
config.combineMode,
config
);
// 3. Validation & Filtering - use standard pipeline utilities
const { validItems, invalidItems, validationErrors } =
@@ -401,7 +421,8 @@ export class MultiSourceOrchestrator {
*/
private combineItems(
itemGroups: CollectionItem[][],
combineMode: 'interleaved' | 'list_order' | 'randomised' | 'cycle_lists'
combineMode: 'interleaved' | 'list_order' | 'randomised' | 'cycle_lists',
parentConfig: MultiSourceCollectionConfig
): CollectionItem[] {
switch (combineMode) {
case 'interleaved':
@@ -421,7 +442,7 @@ export class MultiSourceOrchestrator {
case 'cycle_lists':
// Only one source active at a time, rotates each sync
return this.cycleListsItems(itemGroups);
return this.cycleListsItems(itemGroups, parentConfig.id);
default:
return this.concatenateItems(itemGroups);
@@ -470,18 +491,27 @@ export class MultiSourceOrchestrator {
}
/**
* Cycle lists: only show one source at a time, deterministically rotate
* Cycle lists: only show one source at a time, rotate on each sync execution
*/
private cycleListsItems(itemGroups: CollectionItem[][]): CollectionItem[] {
private cycleListsItems(
itemGroups: CollectionItem[][],
configId: string
): CollectionItem[] {
if (itemGroups.length === 0) return [];
// Use a deterministic rotation based on current date (changes daily)
const today = new Date();
const dayOfYear = Math.floor(
(today.getTime() - new Date(today.getFullYear(), 0, 0).getTime()) /
86400000
);
const selectedIndex = dayOfYear % itemGroups.length;
// Get current sync counter for this collection
const syncCounter = getCollectionSyncCounter(configId);
// Select source based on sync iteration count
const selectedIndex = syncCounter % itemGroups.length;
logger.debug(`Cycle lists selection for ${configId}`, {
label: 'Multi-Source Orchestrator',
configId,
syncCounter,
selectedIndex,
totalSources: itemGroups.length,
});
return itemGroups[selectedIndex] || [];
}
@@ -119,7 +119,7 @@ const AutoRequestSection = ({
className="form-checkbox"
id="enableGrabMissingItems"
/>
<span className="ml-2 text-sm font-medium text-gray-300">
<span className="ml-2 text-sm text-gray-300">
{intl.formatMessage(messages.grabMissingItems)}
</span>
</label>
@@ -133,7 +133,7 @@ const AutoRequestSection = ({
<>
{/* Media Type Processing Options */}
<div className="mb-6">
<div className="mb-3 text-sm font-medium text-gray-300">
<div className="mb-3 text-sm font-medium text-gray-200">
Content Processing
</div>
<div className="space-y-3">
@@ -178,7 +178,7 @@ const AutoRequestSection = ({
{/* Position Limit */}
<div className="mb-6">
<div className="mb-2 text-sm font-medium text-gray-300">
<div className="mb-2 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.positionLimit)}
</div>
<div className="form-input-field">
@@ -202,7 +202,7 @@ const AutoRequestSection = ({
{/* TV Season Limit - only show when TV processing is enabled */}
{values.searchMissingTV && (
<div className="mb-6">
<div className="mb-2 text-sm font-medium text-gray-300">
<div className="mb-2 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.tvSeasonLimit)}
</div>
<div className="form-input-field">
@@ -227,7 +227,7 @@ const AutoRequestSection = ({
{/* Seasons Per Show Limit - only show when TV processing is enabled */}
{values.searchMissingTV && (
<div className="mb-6">
<div className="mb-2 text-sm font-medium text-gray-300">
<div className="mb-2 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.seasonsPerShow)}
</div>
<div className="form-input-field">
@@ -251,7 +251,7 @@ const AutoRequestSection = ({
{/* Step 3: Download Method Selection */}
<div className="mb-6">
<div className="mb-3 text-sm font-medium text-gray-300">
<div className="mb-3 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.downloadMethod)}
</div>
<div className="space-y-3">
@@ -307,7 +307,7 @@ const AutoRequestSection = ({
{/* Step 4: Overseerr-Specific Options (only show when Overseerr mode is selected) */}
{values.downloadMode === 'overseerr' && (
<div className="mb-6">
<div className="mb-3 text-sm font-medium text-gray-300">
<div className="mb-3 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.overseerrOptions)}
</div>
<div className="space-y-3">
@@ -187,10 +187,7 @@ const CollectionTypeSection = ({
<div className="space-y-4">
{/* Collection Type */}
<div>
<label
htmlFor="type"
className="mb-2 block text-sm font-medium text-gray-300"
>
<label htmlFor="type" className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.collectionType)}{' '}
<span className="text-red-500">*</span>
</label>
@@ -198,7 +195,7 @@ const CollectionTypeSection = ({
as="select"
id="type"
name="type"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newType = e.target.value;
const oldType = values.type;
@@ -235,10 +232,7 @@ const CollectionTypeSection = ({
{/* Collection Sub-Type */}
{values.type && subtypeOptions.length > 0 && (
<div>
<label
htmlFor="subtype"
className="mb-2 block text-sm font-medium text-gray-300"
>
<label htmlFor="subtype" className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.collectionSubtype)}{' '}
<span className="text-red-500">*</span>
</label>
@@ -246,7 +240,7 @@ const CollectionTypeSection = ({
as="select"
id="subtype"
name="subtype"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newSubtype = e.target.value;
setFieldValue('subtype', newSubtype);
@@ -318,7 +312,7 @@ const CollectionTypeSection = ({
<div>
<label
htmlFor="customDays"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
Number of Days <span className="text-red-500">*</span>
</label>
@@ -329,13 +323,13 @@ const CollectionTypeSection = ({
placeholder="30"
min="1"
max="365"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
<div>
<label
htmlFor="minimumPlays"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
Minimum Play Count <span className="text-red-500">*</span>
</label>
@@ -346,7 +340,7 @@ const CollectionTypeSection = ({
placeholder="3"
min="1"
max="100"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
</div>
@@ -88,7 +88,7 @@ const CustomUrlSection = ({
<div>
<label
htmlFor="traktCustomListUrl"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customTraktListUrl)}{' '}
<span className="text-red-500">*</span>
@@ -99,7 +99,7 @@ const CustomUrlSection = ({
id="traktCustomListUrl"
name="traktCustomListUrl"
placeholder="https://trakt.tv/users/username/lists/listname or https://trakt.tv/lists/official/collection-name"
className="flex-1 rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="flex-1 rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
{fetchTraktTitle && (
<button
@@ -133,7 +133,7 @@ const CustomUrlSection = ({
<div>
<label
htmlFor="tmdbCustomListUrl"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customTmdbCollectionUrl)}{' '}
<span className="text-red-500">*</span>
@@ -144,7 +144,7 @@ const CustomUrlSection = ({
id="tmdbCustomListUrl"
name="tmdbCustomListUrl"
placeholder="https://www.themoviedb.org/collection/12345"
className="flex-1 rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="flex-1 rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
{fetchTmdbTitle && (
<button
@@ -177,7 +177,7 @@ const CustomUrlSection = ({
<div>
<label
htmlFor="imdbCustomListUrl"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customImdbListUrl)}{' '}
<span className="text-red-500">*</span>
@@ -188,7 +188,7 @@ const CustomUrlSection = ({
id="imdbCustomListUrl"
name="imdbCustomListUrl"
placeholder="https://www.imdb.com/list/ls123456789/"
className="flex-1 rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="flex-1 rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
{fetchImdbTitle && (
<button
@@ -222,7 +222,7 @@ const CustomUrlSection = ({
<div>
<label
htmlFor="letterboxdCustomListUrl"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customLetterboxdListUrl)}{' '}
<span className="text-red-500">*</span>
@@ -233,7 +233,7 @@ const CustomUrlSection = ({
id="letterboxdCustomListUrl"
name="letterboxdCustomListUrl"
placeholder="https://letterboxd.com/username/list/listname/"
className="flex-1 rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="flex-1 rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
{fetchLetterboxdTitle && (
<button
@@ -69,24 +69,24 @@ const LibraryCheckboxDropdown = ({
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
control: (base, state) => ({
...base,
backgroundColor: '#374151',
backgroundColor: '#44403c',
borderColor: error
? '#ef4444'
: state.isFocused
? '#6366f1'
: '#4b5563',
? '#ea580c'
: '#78716c',
'&:hover': {
borderColor: error ? '#ef4444' : '#6366f1',
borderColor: error ? '#ef4444' : '#ea580c',
},
boxShadow: state.isFocused
? error
? '0 0 0 1px #ef4444'
: '0 0 0 1px #6366f1'
: '0 0 0 1px #ea580c'
: 'none',
}),
menu: (base) => ({
...base,
backgroundColor: '#374151',
backgroundColor: '#44403c',
border: '1px solid #4b5563',
}),
option: (base, state) => {
@@ -119,7 +119,7 @@ const LibraryCheckboxDropdown = ({
},
multiValue: (base) => ({
...base,
backgroundColor: '#4b5563',
backgroundColor: '#57534e',
color: 'white',
}),
multiValueLabel: (base) => ({
@@ -128,7 +128,7 @@ const LibraryCheckboxDropdown = ({
}),
multiValueRemove: (base) => ({
...base,
color: '#9ca3af',
color: '#a8a29e',
'&:hover': {
backgroundColor: '#ef4444',
color: 'white',
@@ -217,10 +217,10 @@ const LibrarySelectionSection = ({
// Enhanced form - read-only display
return (
<div>
<label className="mb-2 block text-sm font-medium text-gray-300">
<label className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.librarySelection)}
</label>
<div className="rounded-md border border-gray-600 bg-gray-700 p-3">
<div className="rounded-md border border-stone-500 bg-stone-800 p-3">
<div className="text-sm text-gray-300">
{values.libraryIds && Array.isArray(values.libraryIds) ? (
values.libraryIds.includes('all') ? (
@@ -259,7 +259,7 @@ const LibrarySelectionSection = ({
// Regular form - editable
return (
<div>
<label className="mb-2 block text-sm font-medium text-gray-300">
<label className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.librarySelection)}{' '}
<span className="text-red-500">*</span>
</label>
@@ -73,7 +73,7 @@ const NetworksPlatformSelect = ({
id={`source-platform-${index}`}
name={`sources[${index}].subtype`}
value={value}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
disabled={isLoadingPlatforms}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
onChange(e.target.value);
@@ -308,10 +308,10 @@ const MultiSourceConfigSection = ({
{sources.map((source, index) => (
<div
key={source.id}
className="space-y-4 rounded-lg border border-gray-600 bg-gray-800 p-4"
className="space-y-4 rounded-lg border border-slate-500 bg-stone-800 p-4"
>
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium text-gray-100">
<h5 className="text-sm font-medium text-gray-200">
Source {index + 1}
</h5>
{sources.length > 1 && (
@@ -328,7 +328,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-type-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.sourceType)}{' '}
<span className="text-red-500">*</span>
@@ -337,7 +337,7 @@ const MultiSourceConfigSection = ({
as="select"
id={`source-type-${index}`}
name={`sources[${index}].type`}
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newType = e.target.value;
setFieldValue(`sources[${index}].type`, newType);
@@ -362,7 +362,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-subtype-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.sourceSubtype)}{' '}
<span className="text-red-500">*</span>
@@ -371,7 +371,7 @@ const MultiSourceConfigSection = ({
as="select"
id={`source-subtype-${index}`}
name={`sources[${index}].subtype`}
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newSubtype = e.target.value;
setFieldValue(`sources[${index}].subtype`, newSubtype);
@@ -410,7 +410,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-url-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customUrl)}{' '}
<span className="text-red-500">*</span>
@@ -422,7 +422,7 @@ const MultiSourceConfigSection = ({
placeholder={intl.formatMessage(
messages.customUrlPlaceholder
)}
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFieldValue(
`sources[${index}].customUrl`,
@@ -438,7 +438,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-country-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.networksCountry)}{' '}
<span className="text-red-500">*</span>
@@ -447,7 +447,7 @@ const MultiSourceConfigSection = ({
as="select"
id={`source-country-${index}`}
name={`sources[${index}].networksCountry`}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newCountry = e.target.value;
setFieldValue(
@@ -491,7 +491,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-platform-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.networksPlatform)}{' '}
<span className="text-red-500">*</span>
@@ -516,7 +516,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-timeperiod-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.timePeriod)}{' '}
<span className="text-red-500">*</span>
@@ -525,7 +525,7 @@ const MultiSourceConfigSection = ({
as="select"
id={`source-timeperiod-${index}`}
name={`sources[${index}].timePeriod`}
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
setFieldValue(
`sources[${index}].timePeriod`,
@@ -546,7 +546,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-days-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.customDays)}
</label>
@@ -557,7 +557,7 @@ const MultiSourceConfigSection = ({
placeholder="30"
min="1"
max="365"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFieldValue(
`sources[${index}].customDays`,
@@ -569,7 +569,7 @@ const MultiSourceConfigSection = ({
<div>
<label
htmlFor={`source-plays-${index}`}
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.minimumPlays)}
</label>
@@ -580,7 +580,7 @@ const MultiSourceConfigSection = ({
placeholder="3"
min="1"
max="100"
className="w-full rounded-md border border-slate-500 bg-slate-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFieldValue(
`sources[${index}].minimumPlays`,
@@ -602,7 +602,7 @@ const MultiSourceConfigSection = ({
</div>
<div>
<div className="mb-3 block text-sm font-medium text-gray-300">
<div className="mb-3 block text-sm font-medium text-gray-200">
{intl.formatMessage(messages.combineMode)}
</div>
<div className="space-y-3">
@@ -75,7 +75,7 @@ const NetworksConfigSection = ({
<div>
<label
htmlFor="networksCountry"
className="mb-2 block text-sm font-medium text-gray-300"
className="mb-2 block text-sm text-gray-300"
>
{intl.formatMessage(messages.networksCountry)}{' '}
<span className="text-red-500">*</span>
@@ -84,7 +84,7 @@ const NetworksConfigSection = ({
as="select"
id="networksCountry"
name="networksCountry"
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newCountry = e.target.value;
setFieldValue('networksCountry', newCountry);
@@ -132,10 +132,7 @@ const NetworksConfigSection = ({
{/* Platform Selection - Only show if country is selected */}
{values.networksCountry && (
<div>
<label
htmlFor="subtype"
className="mb-2 block text-sm font-medium text-gray-300"
>
<label htmlFor="subtype" className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.networksPlatform)}{' '}
<span className="text-red-500">*</span>
</label>
@@ -143,7 +140,7 @@ const NetworksConfigSection = ({
as="select"
id="subtype"
name="subtype"
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
disabled={isLoadingPlatforms}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newPlatform = e.target.value;
@@ -241,8 +241,8 @@ const TemplateSection = ({
)}
{/* Template preview */}
<div className="mt-3 rounded-md bg-gray-700 p-3">
<h5 className="mb-2 text-sm font-medium text-white">Preview:</h5>
<div className="mt-3 rounded-md bg-stone-800 p-3">
<h5 className="mb-2 text-sm font-medium text-gray-200">Preview:</h5>
<div className="text-sm text-gray-300">
{(() => {
// Get the actual template being used (same logic as dropdown)
@@ -73,10 +73,7 @@ const TimePeriodSection = ({
return (
<div>
<label
htmlFor="timePeriod"
className="mb-2 block text-sm font-medium text-gray-300"
>
<label htmlFor="timePeriod" className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.timePeriod)}{' '}
<span className="text-red-500">*</span>
</label>
@@ -84,7 +81,7 @@ const TimePeriodSection = ({
as="select"
id="timePeriod"
name="timePeriod"
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const newTimePeriod = e.target.value;
setFieldValue('timePeriod', newTimePeriod);
@@ -16,17 +16,23 @@ const messages = defineMessages({
timeRestrictions: 'Time Restrictions',
alwaysActive: 'Always Active',
removeFromPlex: 'Remove from Plex when inactive',
dateRangesTitle: 'Date Ranges for Collection to be active',
weeklyScheduleTitle: 'Days of the Week for Collection to be active',
dateRangesTitle: 'Date Ranges',
weeklyScheduleTitle: 'Days of the Week',
addDateRange: '+ Add Date Range',
remove: 'Remove',
timeRestrictionsHelp:
'Time restrictions allow you to control when and how collections appear in Plex based on date ranges and weekly schedules. You can choose to either remove collections completely when inactive, or change their visibility settings.',
'Control when this collection is active in Plex. By default, collections are always active.',
dateRangesHelp:
"Specify date ranges when the collection should be active (format: DD-MM). If you want the collection to be active year round but only on certain days of the week, don't add a date range",
weeklyScheduleHelp:
'Choose which days of the week the collection should be active. All days are selected by default.',
inactiveVisibilityHelp:
'When the collection is inactive (outside time restrictions), control where it appears in Plex.',
customSyncSchedule: 'Custom Sync Schedule',
customSyncEnabled: 'Enable custom sync timing',
customSyncInterval: 'Sync every (hours)',
customSyncHelp:
'When enabled, this collection will sync independently from the main sync schedule. Supports decimal hours (e.g., 0.5 for 30 minutes, 2.5 for 2.5 hours).',
'Override the default sync schedule for this collection. Use decimals for partial hours (0.5 = 30 minutes). This will also cycle the list for Random Lists and Multi-Source Collections in "Cycle Lists" mode.',
});
interface DateRange {
@@ -93,13 +99,13 @@ const TimeRestrictionsSection = ({
removeFromPlexWhenInactive: false,
dateRanges: [],
weeklySchedule: {
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
sunday: false,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: true,
sunday: true,
},
};
@@ -147,13 +153,13 @@ const TimeRestrictionsSection = ({
checked: boolean
) => {
const currentSchedule = timeRestriction.weeklySchedule || {
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
sunday: false,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: true,
sunday: true,
};
updateTimeRestriction({
alwaysActive: false,
@@ -176,6 +182,10 @@ const TimeRestrictionsSection = ({
return (
<>
<div className="label-tip">
{intl.formatMessage(messages.timeRestrictionsHelp)}
</div>
<div className="form-input-field">
<label className="inline-flex items-center">
<input
@@ -210,7 +220,7 @@ const TimeRestrictionsSection = ({
}}
className="form-checkbox"
/>
<span className="ml-2 text-sm text-gray-400">
<span className="ml-2 text-sm text-gray-300">
{intl.formatMessage(messages.removeFromPlex)}
</span>
</label>
@@ -231,8 +241,7 @@ const TimeRestrictionsSection = ({
descriptionKey="inactiveVisibilityDescription"
/>
<p className="mt-2 text-xs text-gray-400">
Control where this collection appears when inactive (outside time
restrictions)
{intl.formatMessage(messages.inactiveVisibilityHelp)}
</p>
</div>
)}
@@ -242,7 +251,7 @@ const TimeRestrictionsSection = ({
<div className="mt-4 space-y-4">
{/* Date Ranges */}
<div>
<div className="mb-2 block text-sm font-medium text-gray-300">
<div className="mb-2 block text-sm font-medium text-gray-200">
{intl.formatMessage(messages.dateRangesTitle)}
</div>
@@ -288,11 +297,14 @@ const TimeRestrictionsSection = ({
>
{intl.formatMessage(messages.addDateRange)}
</button>
<p className="mt-2 text-xs text-gray-400">
{intl.formatMessage(messages.dateRangesHelp)}
</p>
</div>
{/* Weekly Schedule */}
<div>
<div className="mb-2 block text-sm font-medium text-gray-300">
<div className="mb-2 block text-sm font-medium text-gray-200">
{intl.formatMessage(messages.weeklyScheduleTitle)}
</div>
@@ -301,7 +313,7 @@ const TimeRestrictionsSection = ({
<label key={day.key} className="inline-flex items-center">
<input
type="checkbox"
checked={timeRestriction.weeklySchedule?.[day.key] ?? false}
checked={timeRestriction.weeklySchedule?.[day.key] ?? true}
onChange={(e) =>
updateWeeklySchedule(day.key, e.target.checked)
}
@@ -313,6 +325,9 @@ const TimeRestrictionsSection = ({
</label>
))}
</div>
<p className="mt-2 text-xs text-gray-400">
{intl.formatMessage(messages.weeklyScheduleHelp)}
</p>
</div>
</div>
)}
@@ -320,20 +335,22 @@ const TimeRestrictionsSection = ({
{/* Custom Sync Schedule Section - Available for all collections */}
{true && (
<div className="mt-6">
<label className="mb-4 block text-sm font-medium text-gray-300">
<label className="mb-4 block text-sm font-medium text-gray-200">
{intl.formatMessage(messages.customSyncSchedule)}
</label>
<div className="space-y-4">
<div className="flex items-center">
<Field
type="checkbox"
name="customSyncSchedule.enabled"
className="rounded border-gray-600 bg-gray-700 text-orange-500 focus:ring-orange-500"
/>
<span className="ml-3 text-sm text-gray-300">
{intl.formatMessage(messages.customSyncEnabled)}
</span>
<div className="form-input-field">
<label className="inline-flex items-center">
<Field
type="checkbox"
name="customSyncSchedule.enabled"
className="form-checkbox"
/>
<span className="ml-2 text-sm text-gray-300">
{intl.formatMessage(messages.customSyncEnabled)}
</span>
</label>
</div>
{(
@@ -342,7 +359,7 @@ const TimeRestrictionsSection = ({
}
).customSyncSchedule?.enabled && (
<div>
<label className="mb-2 block text-sm font-medium text-gray-300">
<label className="mb-2 block text-sm text-gray-300">
{intl.formatMessage(messages.customSyncInterval)}
</label>
<Field
@@ -351,9 +368,9 @@ const TimeRestrictionsSection = ({
step="0.5"
min="0.5"
max="168"
className="w-32 rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
className="w-32 rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
<p className="mt-2 text-sm text-gray-400">
<p className="mt-2 text-xs text-gray-400">
{intl.formatMessage(messages.customSyncHelp)}
</p>
</div>
@@ -361,10 +378,6 @@ const TimeRestrictionsSection = ({
</div>
</div>
)}
<div className="label-tip">
{intl.formatMessage(messages.timeRestrictionsHelp)}
</div>
</>
);
};
@@ -764,7 +764,7 @@ const AllCollectionsView: React.FC = () => {
{isHub && (
<Badge
badgeType="default"
className="!bg-gray-600/20 text-xs !text-gray-300"
className="!bg-stone-600/20 text-xs !text-stone-300"
>
Plex Default
</Badge>
@@ -772,7 +772,7 @@ const AllCollectionsView: React.FC = () => {
{isPreExisting && (
<Badge
badgeType="default"
className="!border !border-orange-500 !bg-gray-600/20 text-xs !text-gray-300"
className="!border !border-orange-500 !bg-stone-600/20 text-xs !text-stone-300"
>
Pre-Existing
</Badge>
@@ -125,7 +125,7 @@ const CollectionStatsGrid: React.FC = () => {
onChange={(e) => setDays(Number(e.target.value) || 30)}
min="0"
max="9999"
className="w-16 rounded border border-gray-600 bg-gray-700 px-2 py-1 text-sm text-white focus:border-orange-400 focus:outline-none"
className="w-16 rounded border border-stone-500 bg-stone-700 px-2 py-1 text-sm text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="30"
/>
</div>
@@ -324,7 +324,7 @@ const MissingItemsModal: React.FC<MissingItemsModalProps> = ({
onChange={(e) =>
handleFilterChange('mediaType', e.target.value)
}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-sm text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="">
{intl.formatMessage(messages.allMediaTypes)}
@@ -348,7 +348,7 @@ const MissingItemsModal: React.FC<MissingItemsModalProps> = ({
onChange={(e) =>
handleFilterChange('status', e.target.value)
}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-sm text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="">
{intl.formatMessage(messages.allStatuses)}
@@ -387,7 +387,7 @@ const MissingItemsModal: React.FC<MissingItemsModalProps> = ({
onChange={(e) =>
handleFilterChange('collectionSource', e.target.value)
}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-sm text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="">
{intl.formatMessage(messages.allSources)}
@@ -417,7 +417,7 @@ const MissingItemsModal: React.FC<MissingItemsModalProps> = ({
onChange={(e) =>
handleFilterChange('requestService', e.target.value)
}
className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white"
className="w-full rounded-md border border-stone-500 bg-stone-700 px-3 py-2 text-sm text-white focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="">
{intl.formatMessage(messages.allServices)}
+2 -2
View File
@@ -297,7 +297,7 @@
input[type='password'],
select,
textarea {
@apply block w-full min-w-0 flex-1 rounded-md border border-slate-500 bg-slate-700 text-white transition duration-150 ease-in-out sm:text-sm sm:leading-5;
@apply block w-full min-w-0 flex-1 rounded-md border border-stone-500 bg-stone-700 text-white transition duration-150 ease-in-out sm:text-sm sm:leading-5;
}
input.rounded-l-only,
@@ -411,7 +411,7 @@
}
.react-select-container .react-select__multi-value {
@apply rounded-md border border-slate-500 bg-slate-800;
@apply rounded-md border border-stone-500 bg-stone-800;
}
.react-select-container .react-select__multi-value__label {