PlexPlaylist_UI subtree merge

feat: Implement internationalization and rename project

Merge commit 'a745adc1ab02adbd17ed19574f47070f87eba50b'
This commit is contained in:
2025-12-09 05:19:21 +09:00
13 changed files with 569 additions and 143 deletions
+21 -19
View File
@@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { PlexConnectionSettings, PlexServerConnection, PlexLibrary } from '../types';
import { apiService } from '../services/api';
import { X, Server, Lock, User, Key, Globe, Eye, EyeOff, CheckCircle, Library, ChevronDown, ChevronRight, Settings, Loader2 } from 'lucide-react';
import { useLanguage } from '../LanguageContext';
interface ConnectionModalProps {
isOpen: boolean;
@@ -12,6 +13,7 @@ interface ConnectionModalProps {
}
const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onConnectSuccess, onShowMessage }) => {
const { t } = useLanguage();
const [formData, setFormData] = useState<PlexConnectionSettings>({
protocol: 'http',
address: '',
@@ -71,7 +73,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
const updatedInfo = { ...connectedServerInfo, libraryName: lib.title };
setConnectedServerInfo(updatedInfo);
onConnectSuccess(updatedInfo);
onShowMessage(`Library switched to ${lib.title}`);
onShowMessage(t('toasts.librarySwitched', { library: lib.title }));
}
};
@@ -90,7 +92,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
abortControllerRef.current.abort();
abortControllerRef.current = null;
setIsConnecting(false);
setError("Connection cancelled by user.");
setError(t('toasts.connectionCancelled'));
}
return;
}
@@ -119,7 +121,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
const info = result.data.serverInfo;
setConnectedServerInfo(info);
onShowMessage(`Successfully connected to ${info.name || 'Plex Server'}`);
onShowMessage(t('toasts.connectedTo', { name: info.name || 'Plex Server' }));
const libs = info.libraries || [];
setLibraries(libs);
@@ -134,7 +136,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
onConnectSuccess(info);
}
} else {
setError(result.message || "Connection failed");
setError(result.message || t('server.connectionFailed'));
}
};
@@ -154,7 +156,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
<div className="px-6 py-4 bg-gray-800 border-b border-gray-700 flex items-center justify-between flex-none">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Server size={18} className={isConnected ? "text-green-400" : "text-plex-orange"} />
{isConnected ? 'Server Connected' : 'Connect Plex Server'}
{isConnected ? t('connection.titleConnected') : t('connection.titleConnect')}
</h3>
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
<X size={20} />
@@ -173,7 +175,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
{/* Server Connection */}
<div className="space-y-3">
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Server Details</label>
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{t('connection.serverDetails')}</label>
<div className="grid grid-cols-4 gap-3">
<div className="col-span-1">
<select
@@ -197,7 +199,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
name="address"
required
disabled={isConnected || isConnecting}
placeholder="IP Address or Domain"
placeholder={t('connection.address')}
value={formData.address}
onChange={handleChange}
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
@@ -211,7 +213,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
type="text"
name="port"
disabled={isConnected || isConnecting}
placeholder="Port (e.g. 32400)"
placeholder={t('connection.port')}
value={formData.port}
onChange={handleChange}
className={`w-full h-10 px-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
@@ -223,7 +225,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
{/* Authentication */}
<div className="space-y-3">
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Authentication</label>
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{t('connection.authentication')}</label>
{/* Token */}
<div className="relative">
@@ -234,7 +236,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
type="text"
name="token"
disabled={isConnected || isConnecting}
placeholder="X-Plex-Token (Optional)"
placeholder={t('connection.token')}
value={formData.token}
onChange={handleChange}
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all font-mono ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
@@ -256,7 +258,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
type="text"
name="username"
disabled={isTokenProvided || isConnecting}
placeholder="Username / Email"
placeholder={t('connection.username')}
value={formData.username}
onChange={handleChange}
className={`w-full h-10 pl-9 pr-3 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
@@ -272,7 +274,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
type={showPassword ? "text" : "password"}
name="password"
disabled={isTokenProvided || isConnecting}
placeholder="Password"
placeholder={t('connection.password')}
value={formData.password}
onChange={handleChange}
className={`w-full h-10 pl-9 pr-10 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
@@ -300,7 +302,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
>
<div className="flex items-center gap-2">
<Settings size={14} />
<span>Advanced Options</span>
<span>{t('connection.advanced')}</span>
</div>
{showAdvanced ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
@@ -308,7 +310,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
{showAdvanced && (
<div className="p-3 bg-gray-900/50 space-y-3 animate-in slide-in-from-top-2">
<div>
<label className="text-xs text-gray-500 mb-1 block">Connection Timeout (Seconds)</label>
<label className="text-xs text-gray-500 mb-1 block">{t('connection.timeout')}</label>
<input
type="number"
min="1"
@@ -337,15 +339,15 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
{isConnecting ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>Connecting... <span className="opacity-75 font-normal ml-1">(Cancel)</span></span>
<span>{t('connection.connecting')} <span className="opacity-75 font-normal ml-1">({t('common.cancel')})</span></span>
</>
) : 'Connect Server'}
) : t('connection.connectBtn')}
</button>
) : (
<div className="mt-2 p-2 bg-green-500/10 border border-green-500/20 rounded-lg text-center">
<p className="text-green-400 text-sm font-semibold flex items-center justify-center gap-2">
<CheckCircle size={16} />
Connected Successfully
{t('connection.connectedSuccess')}
</p>
</div>
)}
@@ -354,7 +356,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
{/* Library Selection - Appears after connection */}
{isConnected && libraries.length > 0 && (
<div className="mt-6 pt-5 border-t border-gray-700 animate-in slide-in-from-top-2 fade-in">
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider block mb-2">Select Library to Sync</label>
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider block mb-2">{t('connection.selectLibrary')}</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Library size={14} className="text-plex-orange" />
@@ -378,7 +380,7 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onCo
onClick={onClose}
className="px-6 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors border border-gray-600 hover:border-gray-500"
>
Done
{t('common.done')}
</button>
</div>
</div>
+6 -3
View File
@@ -1,12 +1,15 @@
import React from 'react';
import { Playlist } from '../types';
import { Disc3, Clock } from 'lucide-react';
import { useLanguage } from '../LanguageContext';
interface PlaylistCardProps {
playlist: Playlist;
}
const PlaylistCard: React.FC<PlaylistCardProps> = ({ playlist }) => {
const { t } = useLanguage();
return (
<div className="group flex flex-col w-full p-2.5 bg-gray-800/60 rounded-md border border-gray-700/50 hover:bg-gray-700 hover:border-plex-orange/50 transition-all duration-200 cursor-pointer shadow-sm">
<div className="flex items-center justify-between">
@@ -16,11 +19,11 @@ const PlaylistCard: React.FC<PlaylistCardProps> = ({ playlist }) => {
</div>
<div className="flex items-center mt-1.5 space-x-4 text-xs text-gray-500 group-hover:text-gray-400">
<span className="flex items-center" title="Track Count">
<span className="flex items-center" title={t('playlist.trackCount')}>
<Disc3 size={12} className="mr-1.5 opacity-70" />
{playlist.trackCount}
</span>
<span className="flex items-center" title="Last Updated">
<span className="flex items-center" title={t('playlist.lastUpdated')}>
<Clock size={12} className="mr-1.5 opacity-70" />
{new Date(playlist.lastUpdated).toLocaleDateString()}
</span>
@@ -29,4 +32,4 @@ const PlaylistCard: React.FC<PlaylistCardProps> = ({ playlist }) => {
);
};
export default PlaylistCard;
export default PlaylistCard;
+12 -10
View File
@@ -3,6 +3,7 @@ import React from 'react';
import { Playlist, ServerType, PlexServerConnection } from '../types';
import PlaylistCard from './PlaylistCard';
import { RefreshCw, Server, Cloud, WifiOff, X } from 'lucide-react';
import { useLanguage } from '../LanguageContext';
interface ServerPanelProps {
type: ServerType;
@@ -14,6 +15,7 @@ interface ServerPanelProps {
}
const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, onRefresh, onCancel, serverInfo }) => {
const { t } = useLanguage();
const isLocal = type === ServerType.LOCAL;
let Icon = isLocal ? Server : Cloud;
@@ -28,17 +30,17 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
let displaySubtitle: React.ReactNode = null;
if (isLocal) {
displayTitle = 'Local Server';
displayTitle = t('server.local');
displaySubtitle = (
<p className="text-xs text-gray-400 font-medium mt-0.5 md:mt-0 md:ml-0">
{playlists.length} Playlists
{t('server.playlists', { count: playlists.length })}
</p>
);
} else {
// Cloud Logic
if (serverInfo) {
if (serverInfo.isConnected) {
displayTitle = serverInfo.name || 'Cloud Server';
displayTitle = serverInfo.name || t('server.cloud');
displaySubtitle = (
<div className="flex items-center text-xs text-gray-300 font-medium space-x-1.5 truncate mt-0.5 md:mt-0">
<span className="text-plex-orange truncate font-semibold">{serverInfo.libraryName}</span>
@@ -47,20 +49,20 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
</div>
);
} else {
displayTitle = 'Not Connected';
displayTitle = t('server.notConnected');
Icon = WifiOff;
headerColor = 'text-red-400';
displaySubtitle = (
<p className="text-xs text-gray-500 font-medium mt-0.5">
Connection failed
{t('server.connectionFailed')}
</p>
);
}
} else {
displayTitle = 'Cloud Server';
displayTitle = t('server.cloud');
displaySubtitle = (
<p className="text-xs text-gray-500 font-medium mt-0.5">
{isLoading ? 'Connecting...' : 'Waiting...'}
{isLoading ? t('server.connecting') : t('server.waiting')}
</p>
);
}
@@ -121,7 +123,7 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
: 'text-gray-400 hover:text-white hover:bg-white/10'
}
`}
title={isLoading ? "Cancel Refresh" : "Refresh Playlists"}
title={isLoading ? t('server.cancelRefresh') : t('server.refreshPlaylists')}
>
{isLoading ? (
<div className="relative flex items-center justify-center">
@@ -141,11 +143,11 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
{isLoading && playlists.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500 space-y-3">
<RefreshCw size={24} className="animate-spin text-plex-orange/50" />
<p className="text-xs font-medium tracking-wide uppercase">Syncing...</p>
<p className="text-xs font-medium tracking-wide uppercase">{t('server.syncing')}</p>
</div>
) : playlists.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<p className="text-sm">No playlists found.</p>
<p className="text-sm">{t('server.noPlaylists')}</p>
</div>
) : (
<div className="space-y-2.5 md:space-y-3">
@@ -1,3 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import { SyncStrategy, ReplacementRule, PathMappingConfig, PathMappingRules, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from '../types';
import {
@@ -23,11 +24,12 @@ import {
History,
Eye
} from 'lucide-react';
import { useLanguage } from '../LanguageContext';
interface StrategyOption {
value: SyncStrategy;
label: string;
description: string;
labelKey: string;
descKey: string;
icon: React.ElementType;
color: string;
}
@@ -35,29 +37,29 @@ interface StrategyOption {
const STRATEGIES: StrategyOption[] = [
{
value: SyncStrategy.LOCAL_OVERWRITE,
label: 'Local Overwrite',
description: 'Local playlist completely overwrites Cloud. (No Diff)',
labelKey: 'strategies.localOverwrite.label',
descKey: 'strategies.localOverwrite.desc',
icon: ArrowRightCircle,
color: 'text-blue-400'
},
{
value: SyncStrategy.CLOUD_OVERWRITE,
label: 'Cloud Overwrite',
description: 'Cloud playlist completely overwrites Local. (No Diff)',
labelKey: 'strategies.cloudOverwrite.label',
descKey: 'strategies.cloudOverwrite.desc',
icon: ArrowLeftCircle,
color: 'text-green-400'
},
{
value: SyncStrategy.MERGE_LOCAL,
label: 'Two-way Merge (Local Priority)',
description: 'Merge both. Conflicts resolve to Local version.',
labelKey: 'strategies.mergeLocal.label',
descKey: 'strategies.mergeLocal.desc',
icon: GitMerge,
color: 'text-blue-300'
},
{
value: SyncStrategy.MERGE_CLOUD,
label: 'Two-way Merge (Cloud Priority)',
description: 'Merge both. Conflicts resolve to Cloud version.',
labelKey: 'strategies.mergeCloud.label',
descKey: 'strategies.mergeCloud.desc',
icon: GitMerge,
color: 'text-green-300'
}
@@ -116,6 +118,7 @@ interface MappingGroupEditorProps {
rightPlaceholder?: string;
leftInputClass?: string;
rightInputClass?: string;
t: (key: string) => string;
}
const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
@@ -126,10 +129,11 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
isLocked,
borderColor = "border-gray-700",
bgColor = "bg-gray-900/50",
leftPlaceholder = "Pattern",
rightPlaceholder = "Replace",
leftPlaceholder,
rightPlaceholder,
leftInputClass,
rightInputClass
rightInputClass,
t
}) => {
const handleAdd = () => {
@@ -162,7 +166,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
onClick={handleAdd}
disabled={isLocked}
className="p-1 rounded bg-gray-700/50 hover:bg-gray-600 text-gray-400 hover:text-white transition-colors"
title="Add Rule"
title={t('common.add')}
>
<Plus size={12} />
</button>
@@ -171,14 +175,14 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
<div className="flex-1 space-y-2 overflow-y-auto max-h-32 custom-scrollbar pr-1">
{rules.length === 0 ? (
<div className="text-xs text-gray-600 italic text-center py-2 border border-dashed border-gray-700/50 rounded-lg">
No rules defined.
{t('mapping.noRules')}
</div>
) : (
rules.map((rule) => (
<div key={rule.id} className="flex items-center space-x-1 animate-in slide-in-from-left-1 duration-200">
<input
type="text"
placeholder={leftPlaceholder}
placeholder={leftPlaceholder || t('mapping.pattern')}
value={rule.search}
onChange={(e) => handleUpdate(rule.id, 'search', e.target.value)}
className={`flex-1 min-w-0 border rounded px-1.5 py-1 text-xs focus:outline-none transition-colors ${leftInputClass || defaultInputStyle}`}
@@ -186,7 +190,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
<Link size={12} className="text-gray-600 flex-none opacity-50" />
<input
type="text"
placeholder={rightPlaceholder}
placeholder={rightPlaceholder || t('mapping.replace')}
value={rule.replace}
onChange={(e) => handleUpdate(rule.id, 'replace', e.target.value)}
className={`flex-1 min-w-0 border rounded px-1.5 py-1 text-xs focus:outline-none transition-colors ${rightInputClass || defaultInputStyle}`}
@@ -230,6 +234,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
syncState,
onSync
}) => {
const { t } = useLanguage();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -307,7 +312,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
const handleSelect = (strategy: StrategyOption) => {
if (isLocked) return;
onSelect(strategy.value, strategy.label);
onSelect(strategy.value, t(strategy.labelKey));
};
// --- Path Mapping Handlers ---
@@ -439,7 +444,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-center w-12 h-12 rounded-full bg-gray-800/90 border border-gray-600 hover:border-plex-orange text-gray-300 hover:text-white hover:bg-gray-700/80 transition-all shadow-2xl hover:shadow-plex-orange/30 ring-[6px] md:ring-8 ring-gray-900 backdrop-blur-sm active:scale-95"
title={`Current Strategy: ${selectedOption.label}`}
title={`Current Strategy: ${t(selectedOption.labelKey)}`}
>
<selectedOption.icon size={22} className={selectedOption.color} strokeWidth={2.5} />
<div className="absolute -bottom-1 -right-1 bg-gray-900 rounded-full border border-gray-600 p-[2px] shadow-sm">
@@ -465,7 +470,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
{/* Section 1: Sync Strategy */}
<div className="px-4 py-3 bg-black/20 border-b border-white/5 flex-none">
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-2">Sync Strategy</h3>
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-2">{t('strategies.title')}</h3>
<div className="space-y-1">
{STRATEGIES.map((strategy) => (
<div
@@ -480,7 +485,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex items-center space-x-3 overflow-hidden">
<strategy.icon size={18} className={strategy.color} />
<span className={`text-sm font-medium truncate ${currentStrategy === strategy.value ? 'text-white' : 'text-gray-300 group-hover:text-white'}`}>
{strategy.label}
{t(strategy.labelKey)}
</span>
</div>
@@ -488,7 +493,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="relative group/tooltip">
<HelpCircle size={14} className="text-gray-600 hover:text-gray-400 transition-colors" />
<div className="absolute right-0 bottom-full mb-2 w-48 p-2.5 bg-gray-900 text-xs text-gray-300 rounded-lg shadow-xl border border-gray-700 pointer-events-none opacity-0 group-hover/tooltip:opacity-100 transition-opacity z-50">
{strategy.description}
{t(strategy.descKey)}
</div>
</div>
@@ -504,7 +509,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
{/* Section 1.5: Backup Retention */}
<div className="px-4 py-3 bg-gray-900/40 border-b border-white/5 flex-none">
<div className="flex items-center justify-between mb-3">
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Backup Retention</h3>
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">{t('backup.title')}</h3>
</div>
<div className="flex flex-col space-y-3">
@@ -514,8 +519,8 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<Archive size={16} />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-200">Enable Backups</span>
<span className="text-[10px] text-gray-500">Create a copy before changes</span>
<span className="text-sm font-medium text-gray-200">{t('backup.enable')}</span>
<span className="text-[10px] text-gray-500">{t('backup.enableDesc')}</span>
</div>
</div>
@@ -532,7 +537,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex items-center justify-between p-2.5 rounded-lg bg-black/20 border border-white/5">
<div className="flex items-center space-x-2">
<History size={14} className="text-gray-500" />
<span className="text-xs text-gray-400">Max versions to keep:</span>
<span className="text-xs text-gray-400">{t('backup.maxVersions')}</span>
</div>
<div className="flex items-center space-x-2">
<input
@@ -543,7 +548,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
onChange={(e) => handleUpdateBackup('retentionCount', parseInt(e.target.value) || 1)}
className="w-16 bg-gray-800 border border-gray-700 text-center text-sm rounded py-1 text-white focus:border-plex-orange focus:outline-none"
/>
<span className="text-[10px] text-gray-600 italic">Oldest deleted automatically</span>
<span className="text-[10px] text-gray-600 italic">{t('backup.autoDelete')}</span>
</div>
</div>
</div>
@@ -558,7 +563,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`}
>
<RotateCcw size={12} />
<span>Revert</span>
<span>{t('common.revert')}</span>
</button>
<button
onClick={handleSaveBackupClick}
@@ -569,7 +574,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`}
>
<Save size={12} />
<span>Save</span>
<span>{t('common.save')}</span>
</button>
</div>
</div>
@@ -578,14 +583,14 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
{/* Section 2: Path Mapping (Tabs + Grid) */}
<div className="p-4 bg-gray-900/40 border-b border-white/5 flex-none">
<div className="flex items-center justify-between mb-3">
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Path Mapping</h3>
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">{t('mapping.title')}</h3>
</div>
{/* Tabs for Path Mapping Mode */}
<div className="flex space-x-1 bg-black/30 p-1 rounded-lg mb-4">
{[
{ id: PathMappingMode.SIMPLE, label: 'Simple Mapping', icon: Type },
{ id: PathMappingMode.REGEX, label: 'Regex Rules', icon: Code2 },
{ id: PathMappingMode.SIMPLE, label: t('mapping.simple'), icon: Type },
{ id: PathMappingMode.REGEX, label: t('mapping.regex'), icon: Code2 },
].map((tab) => (
<button
key={tab.id}
@@ -608,17 +613,18 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
// Simple Mode: Single Editor
<div className="animate-in fade-in duration-200">
<MappingGroupEditor
title="Path Mapping"
subtitle="Map Local paths to Cloud paths using simple string matching"
title={t('mapping.simpleTitle')}
subtitle={t('mapping.simpleSubtitle')}
rules={simpleRules}
onChange={updateSimpleGroup}
isLocked={isLocked}
borderColor={MAPPING_THEME.simple.borderColor}
bgColor={MAPPING_THEME.simple.bgColor}
leftPlaceholder="Local Path"
rightPlaceholder="Cloud Path"
leftPlaceholder={t('mapping.localPath')}
rightPlaceholder={t('mapping.cloudPath')}
leftInputClass={MAPPING_THEME.inputs.local}
rightInputClass={MAPPING_THEME.inputs.cloud}
t={t}
/>
</div>
) : (
@@ -626,44 +632,48 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in duration-200">
{/* Row 1: Pre-Processing */}
<MappingGroupEditor
title="Local Playlist"
subtitle="Pre-Processing (Before Sync)"
title={t('server.local')}
subtitle={t('mapping.regexPre')}
rules={regexRules.localPre}
onChange={(rules) => updateRegexGroup('localPre', rules)}
isLocked={isLocked}
borderColor={MAPPING_THEME.local.borderColor}
bgColor={MAPPING_THEME.local.bgColor}
t={t}
/>
<MappingGroupEditor
title="Remote Playlist"
subtitle="Pre-Processing (Before Sync)"
title={t('server.cloud')}
subtitle={t('mapping.regexPre')}
rules={regexRules.remotePre}
onChange={(rules) => updateRegexGroup('remotePre', rules)}
isLocked={isLocked}
borderColor={MAPPING_THEME.remote.borderColor}
bgColor={MAPPING_THEME.remote.bgColor}
t={t}
/>
{/* Row 2: Post-Processing */}
<MappingGroupEditor
title="Local Playlist"
subtitle="Post-Processing (After Sync / Result)"
title={t('server.local')}
subtitle={t('mapping.regexPost')}
rules={regexRules.localPost}
onChange={(rules) => updateRegexGroup('localPost', rules)}
isLocked={isLocked}
borderColor={MAPPING_THEME.local.borderColor}
bgColor={MAPPING_THEME.local.bgColor}
t={t}
/>
<MappingGroupEditor
title="Remote Playlist"
subtitle="Post-Processing (After Sync / Result)"
title={t('server.cloud')}
subtitle={t('mapping.regexPost')}
rules={regexRules.remotePost}
onChange={(rules) => updateRegexGroup('remotePost', rules)}
isLocked={isLocked}
borderColor={MAPPING_THEME.remote.borderColor}
bgColor={MAPPING_THEME.remote.bgColor}
t={t}
/>
</div>
)}
@@ -679,7 +689,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`}
>
<RotateCcw size={12} />
<span>Revert</span>
<span>{t('common.revert')}</span>
</button>
<button
onClick={handleSaveMappingClick}
@@ -690,7 +700,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`}
>
<Save size={12} />
<span>Save Rules</span>
<span>{t('mapping.saveRules')}</span>
</button>
</div>
</div>
@@ -698,15 +708,15 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
{/* Section 3: Scheduled Tasks */}
<div className="p-4 bg-gray-900/40 border-b border-white/5 flex-none">
<div className="flex items-center justify-between mb-3">
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Scheduled Tasks</h3>
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">{t('schedule.title')}</h3>
</div>
{/* Tabs */}
<div className="flex space-x-1 bg-black/30 p-1 rounded-lg mb-4">
{[
{ id: ScheduleMode.CRON, label: 'Cron', icon: Repeat },
{ id: ScheduleMode.DAILY, label: 'Daily', icon: Clock },
{ id: ScheduleMode.WEEKLY, label: 'Weekly', icon: Calendar },
{ id: ScheduleMode.CRON, label: t('schedule.cron'), icon: Repeat },
{ id: ScheduleMode.DAILY, label: t('schedule.daily'), icon: Clock },
{ id: ScheduleMode.WEEKLY, label: t('schedule.weekly'), icon: Calendar },
].map((tab) => (
<button
key={tab.id}
@@ -729,7 +739,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex flex-col animate-in fade-in duration-200">
{/* Top Row: Label + Switch */}
<div className="flex items-center justify-between mb-3 px-1">
<span className="text-xs text-gray-400 font-medium">Enable Cron Schedule</span>
<span className="text-xs text-gray-400 font-medium">{t('schedule.enableCron')}</span>
<button
onClick={() => toggleScheduleEnable(ScheduleMode.CRON)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.CRON ? 'bg-plex-orange' : 'bg-gray-700'}`}
@@ -762,7 +772,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex flex-col animate-in fade-in duration-200">
{/* Top Row: Label + Switch */}
<div className="flex items-center justify-between mb-3 px-1">
<span className="text-xs text-gray-400 font-medium">Enable Daily Run</span>
<span className="text-xs text-gray-400 font-medium">{t('schedule.enableDaily')}</span>
<button
onClick={() => toggleScheduleEnable(ScheduleMode.DAILY)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.DAILY ? 'bg-plex-orange' : 'bg-gray-700'}`}
@@ -788,7 +798,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="flex flex-col animate-in fade-in duration-200">
{/* Top Row: Label + Switch */}
<div className="flex items-center justify-between mb-3 px-1">
<span className="text-xs text-gray-400 font-medium">Enable Weekly Run</span>
<span className="text-xs text-gray-400 font-medium">{t('schedule.enableWeekly')}</span>
<button
onClick={() => toggleScheduleEnable(ScheduleMode.WEEKLY)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.WEEKLY ? 'bg-plex-orange' : 'bg-gray-700'}`}
@@ -840,8 +850,8 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<Eye size={16} />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-200">Watch Local Changes</span>
<span className="text-[10px] text-gray-500">Auto-sync when local playlist updates</span>
<span className="text-sm font-medium text-gray-200">{t('schedule.watchLocal')}</span>
<span className="text-[10px] text-gray-500">{t('schedule.watchDesc')}</span>
</div>
</div>
<button
@@ -863,7 +873,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`}
>
<RotateCcw size={12} />
<span>Revert</span>
<span>{t('common.revert')}</span>
</button>
<button
onClick={handleSaveScheduleClick}
@@ -874,7 +884,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
: 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`}
>
<Save size={12} />
<span>Save</span>
<span>{t('common.save')}</span>
</button>
</div>
</div>
@@ -896,18 +906,18 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
{isSyncing ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>Sync in Progress...</span>
<span>{t('strategies.syncing')}</span>
</>
) : (
<>
<Zap size={16} fill="currentColor" />
<span>Sync Now</span>
<span>{t('strategies.syncNow')}</span>
</>
)}
</button>
{(isMappingDirty || isBackupDirty) && (
<p className="text-[10px] text-plex-orange text-center mt-2">
Please save pending changes (Backups/Path Mapping) before syncing.
{t('strategies.saveWarning')}
</p>
)}
</div>
@@ -916,4 +926,4 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
);
};
export default StrategySelector;
export default StrategySelector;