Add Path Mapping UI with Simple Mapping and Regex Rules modes

- Updated frontend/types.ts with new types: ReplacementRule, PathMappingRules, PathMappingMode, PathMappingConfig
- Replaced StrategySelector.tsx with new UI featuring:
  - Simple Mapping tab for local/cloud path pairs
  - Regex Rules tab with 4 rule groups (localPre, localPost, remotePre, remotePost)
  - MappingGroupEditor sub-component for editing rule lists
- Updated App.tsx to use PathMappingConfig state instead of RegexReplacement[]
- Updated api.ts to handle new PathMappingConfig structure
- Updated backend config.py with path_mapping field support
- Added /api/settings/path-mapping endpoint in main.py

Co-authored-by: Koha9 <36852125+Koha9@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-30 22:11:29 +00:00
parent c18ff5b2ef
commit 350f1d97e6
6 changed files with 567 additions and 188 deletions
+78 -22
View File
@@ -1,8 +1,7 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, RegexReplacement, PlexConnectionSettings, SyncState, ScheduleSettings, ScheduleMode } from './types';
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, PlexConnectionSettings, SyncState, ScheduleSettings, ScheduleMode } from './types';
import { apiService } from './services/api';
import {
import {
STRIPE_BASE_SPEED,
STRIPE_DECEL_DURATION_MS,
STRIPE_TILE_SIZE,
@@ -10,15 +9,13 @@ import {
SYNC_SUCCESS_TOTAL_MS,
SYNC_ERROR_RESET_MS,
TOAST_AUTO_DISMISS_MS,
TOAST_EXIT_DURATION_MS,
SYNC_BANNER_PADDING_X,
SYNC_BANNER_PADDING_Y,
SYNC_BANNER_MIN_WIDTH,
TOAST_EXIT_DURATION_MS
} from './Config';
import { SYNC_BANNER_PADDING_X, SYNC_BANNER_PADDING_Y, SYNC_BANNER_MIN_WIDTH } from './Config';
import ServerPanel from './components/ServerPanel';
import StrategySelector from './components/StrategySelector';
import ConnectionModal from './components/ConnectionModal';
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock, Eye, EyeOff } from 'lucide-react';
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock, Eye, EyeOff, Type, Code2 } from 'lucide-react';
interface Toast {
id: number;
@@ -141,8 +138,17 @@ const App: React.FC = () => {
// Strategy State
const [currentStrategy, setCurrentStrategy] = useState<SyncStrategy>(SyncStrategy.LOCAL_OVERWRITE);
// Regex State
const [regexReplacements, setRegexReplacements] = useState<RegexReplacement[]>([]);
// Path Mapping State (Includes Simple and Regex Rules)
const [pathMappingConfig, setPathMappingConfig] = useState<PathMappingConfig>({
mode: PathMappingMode.SIMPLE,
simple: [],
regex: {
localPre: [],
localPost: [],
remotePre: [],
remotePost: []
}
});
// Schedule State
const [scheduleSettings, setScheduleSettings] = useState<ScheduleSettings>({
@@ -226,7 +232,7 @@ const App: React.FC = () => {
const result = await apiService.getSettings();
if (result.status === 'success') {
setCurrentStrategy(result.data.strategy);
setRegexReplacements(result.data.regex);
setPathMappingConfig(result.data.pathMapping);
setLocalPath(result.data.localPath || 'playlist');
setConnectionSettings(result.data.connection);
}
@@ -347,14 +353,14 @@ const App: React.FC = () => {
}
};
// Handle Regex Save
const handleSaveRegex = async (replacements: RegexReplacement[]) => {
setRegexReplacements(replacements);
const result = await apiService.saveRegexRules(replacements);
// Handle Path Mapping Save
const handleSavePathMapping = async (config: PathMappingConfig) => {
setPathMappingConfig(config);
const result = await apiService.savePathMapping(config);
if (result.status === 'success') {
addToast('Regex preprocessing rules have been saved.');
addToast('Path mapping rules have been saved.');
} else {
addToast(result.message || 'Failed to save regex rules.');
addToast(result.message || 'Failed to save path mapping rules.');
}
};
@@ -365,7 +371,7 @@ const App: React.FC = () => {
setSyncState(SyncState.SYNCING);
manualSyncInProgress.current = true;
const result = await apiService.syncPlaylists(currentStrategy, regexReplacements, localPath || undefined);
const result = await apiService.syncPlaylists(currentStrategy, pathMappingConfig, localPath || undefined);
manualSyncInProgress.current = false;
@@ -513,6 +519,44 @@ const App: React.FC = () => {
const scheduleInfo = getScheduleDisplayInfo();
// Helper: Calculate Path Mapping Info
const getPathMappingDisplayInfo = (config: PathMappingConfig) => {
let count = 0;
let modeLabel = '';
let Icon = Type;
if (config.mode === PathMappingMode.SIMPLE) {
modeLabel = 'Simple';
count = config.simple.length;
Icon = Type;
} else {
modeLabel = 'Regex';
count = config.regex.localPre.length +
config.regex.localPost.length +
config.regex.remotePre.length +
config.regex.remotePost.length;
Icon = Code2;
}
if (count === 0) {
return {
label: 'Path Mapping',
value: 'Not Set',
active: false,
Icon: Icon
};
}
return {
label: 'Path Mapping',
value: `${modeLabel} (${count})`,
active: true,
Icon: Icon
};
};
const pathMappingInfo = getPathMappingDisplayInfo(pathMappingConfig);
return (
<div className="min-h-screen flex flex-col bg-gray-900 text-gray-100 font-sans overflow-hidden bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-gray-800 via-gray-900 to-black">
@@ -574,7 +618,7 @@ const App: React.FC = () => {
{syncState === SyncState.IDLE ? (
<>
{/* Normal Toolbar */}
{/* Normal Toolbar Left */}
<div className="flex items-center space-x-3">
<div className="p-1.5 rounded-lg shadow-lg bg-gradient-to-br from-plex-orange to-yellow-600 text-gray-900 shadow-plex-orange/20">
<ArrowLeftRight size={24} strokeWidth={2.5} />
@@ -583,9 +627,20 @@ const App: React.FC = () => {
Plex<span className="text-plex-orange">Sync</span>
</h1>
</div>
{/* Normal Toolbar Right */}
<div className="flex items-center gap-4">
{/* Path Mapping Info */}
<div className="flex flex-col items-end hidden md:flex border-r border-gray-700/50 pr-4 mr-1">
<span className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">
{pathMappingInfo.label}
</span>
<div className={`text-xs font-mono flex items-center gap-1.5 ${pathMappingInfo.active ? 'text-plex-orange' : 'text-gray-600'}`}>
{pathMappingInfo.active && <pathMappingInfo.Icon size={12} />}
<span>{pathMappingInfo.value}</span>
</div>
</div>
{/* Schedule Info */}
<div className="flex flex-col items-end mr-2 md:mr-0 hidden md:flex">
<span className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">
@@ -625,6 +680,7 @@ const App: React.FC = () => {
</div>
</>
) : (
/* Syncing / Success Text Banner */
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
<div
className="bg-black shadow-none rounded-none border-none"
@@ -691,8 +747,8 @@ const App: React.FC = () => {
<StrategySelector
currentStrategy={currentStrategy}
onSelect={handleStrategyChange}
savedRegexReplacements={regexReplacements}
onSaveRegex={handleSaveRegex}
savedPathMapping={pathMappingConfig}
onSavePathMapping={handleSavePathMapping}
savedSchedule={scheduleSettings}
onSaveSchedule={handleSaveSchedule}
syncState={syncState}