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:
+78
-22
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user