Compare commits
2 Commits
f0b129a27e
...
e3d3df9ecb
| Author | SHA1 | Date | |
|---|---|---|---|
| e3d3df9ecb | |||
| a14210c458 |
+88
-14
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from './types';
|
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from './types';
|
||||||
import { apiService } from './services/api';
|
import { apiService } from './services/api';
|
||||||
@@ -16,7 +18,8 @@ import { SYNC_BANNER_PADDING_X, SYNC_BANNER_PADDING_Y, SYNC_BANNER_MIN_WIDTH } f
|
|||||||
import ServerPanel from './components/ServerPanel';
|
import ServerPanel from './components/ServerPanel';
|
||||||
import StrategySelector from './components/StrategySelector';
|
import StrategySelector from './components/StrategySelector';
|
||||||
import ConnectionModal from './components/ConnectionModal';
|
import ConnectionModal from './components/ConnectionModal';
|
||||||
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock, Eye, EyeOff, Type, Code2, Archive, Languages } from 'lucide-react';
|
import LoginScreen from './components/LoginScreen';
|
||||||
|
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock, Eye, EyeOff, Type, Code2, Archive, Languages, LogOut, User } from 'lucide-react';
|
||||||
import { useLanguage } from './LanguageContext';
|
import { useLanguage } from './LanguageContext';
|
||||||
|
|
||||||
interface Toast {
|
interface Toast {
|
||||||
@@ -115,6 +118,12 @@ const useStripeAnimation = (syncState: SyncState) => {
|
|||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { t, language, setLanguage } = useLanguage();
|
const { t, language, setLanguage } = useLanguage();
|
||||||
|
|
||||||
|
// Auth State
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState('');
|
||||||
|
|
||||||
|
// App Data State
|
||||||
const [localPlaylists, setLocalPlaylists] = useState<Playlist[]>([]);
|
const [localPlaylists, setLocalPlaylists] = useState<Playlist[]>([]);
|
||||||
const [cloudPlaylists, setCloudPlaylists] = useState<Playlist[]>([]);
|
const [cloudPlaylists, setCloudPlaylists] = useState<Playlist[]>([]);
|
||||||
const [cloudServerInfo, setCloudServerInfo] = useState<PlexServerConnection | undefined>(undefined);
|
const [cloudServerInfo, setCloudServerInfo] = useState<PlexServerConnection | undefined>(undefined);
|
||||||
@@ -198,6 +207,16 @@ const App: React.FC = () => {
|
|||||||
timeoutsRef.current[id] = dismissTimer;
|
timeoutsRef.current[id] = dismissTimer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check auth on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const savedToken = localStorage.getItem('plexsync-token');
|
||||||
|
const savedUser = localStorage.getItem('plexsync-username');
|
||||||
|
if (savedToken && savedUser) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
setCurrentUser(savedUser);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Effect to trigger the "slide down" animation
|
// Effect to trigger the "slide down" animation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const enteringIds = toasts.filter(t => t.entering).map(t => t.id);
|
const enteringIds = toasts.filter(t => t.entering).map(t => t.id);
|
||||||
@@ -236,6 +255,7 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Fetch Local Playlists
|
// Fetch Local Playlists
|
||||||
const refreshLocal = useCallback(async () => {
|
const refreshLocal = useCallback(async () => {
|
||||||
|
if (!isAuthenticated) return;
|
||||||
if (localAbortRef.current) localAbortRef.current.abort();
|
if (localAbortRef.current) localAbortRef.current.abort();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
localAbortRef.current = abortController;
|
localAbortRef.current = abortController;
|
||||||
@@ -247,7 +267,7 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
setLoadingLocal(false);
|
setLoadingLocal(false);
|
||||||
localAbortRef.current = null;
|
localAbortRef.current = null;
|
||||||
}, []);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const cancelLocalRefresh = () => {
|
const cancelLocalRefresh = () => {
|
||||||
if (localAbortRef.current) {
|
if (localAbortRef.current) {
|
||||||
@@ -260,6 +280,7 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Fetch Cloud Playlists and Info
|
// Fetch Cloud Playlists and Info
|
||||||
const refreshCloud = useCallback(async () => {
|
const refreshCloud = useCallback(async () => {
|
||||||
|
if (!isAuthenticated) return;
|
||||||
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
cloudAbortRef.current = abortController;
|
cloudAbortRef.current = abortController;
|
||||||
@@ -281,7 +302,7 @@ const App: React.FC = () => {
|
|||||||
setLoadingCloud(false);
|
setLoadingCloud(false);
|
||||||
cloudAbortRef.current = null;
|
cloudAbortRef.current = null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const cancelCloudRefresh = () => {
|
const cancelCloudRefresh = () => {
|
||||||
if (cloudAbortRef.current) {
|
if (cloudAbortRef.current) {
|
||||||
@@ -292,16 +313,18 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial Load
|
// Initial Load (Only if Authenticated)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshLocal();
|
if (isAuthenticated) {
|
||||||
refreshCloud();
|
refreshLocal();
|
||||||
|
refreshCloud();
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
if (localAbortRef.current) localAbortRef.current.abort();
|
if (localAbortRef.current) localAbortRef.current.abort();
|
||||||
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
||||||
}
|
}
|
||||||
}, [refreshLocal, refreshCloud]);
|
}, [isAuthenticated, refreshLocal, refreshCloud]);
|
||||||
|
|
||||||
// Handle Strategy Change
|
// Handle Strategy Change
|
||||||
const handleStrategyChange = (strategy: SyncStrategy, label: string) => {
|
const handleStrategyChange = (strategy: SyncStrategy, label: string) => {
|
||||||
@@ -364,13 +387,6 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Timing Breakdown:
|
// Timing Breakdown:
|
||||||
// T+0.0s: State is SUCCESS.
|
// T+0.0s: State is SUCCESS.
|
||||||
// - JS Animation loop detects change and begins decelerating speed from 56 -> 0 over 0.5s.
|
|
||||||
// - CSS opacity transitions Yellow -> Green over 0.3s.
|
|
||||||
|
|
||||||
// T+0.5s: Deceleration complete. Speed is 0. Background is static.
|
|
||||||
// We hold this static state for another 0.5s.
|
|
||||||
|
|
||||||
// T+1.0s: Total success duration complete. Disappear.
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSyncState(SyncState.IDLE);
|
setSyncState(SyncState.IDLE);
|
||||||
refreshLocal();
|
refreshLocal();
|
||||||
@@ -390,6 +406,27 @@ const App: React.FC = () => {
|
|||||||
refreshCloud();
|
refreshCloud();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoginSuccess = (token: string, username: string) => {
|
||||||
|
localStorage.setItem('plexsync-token', token);
|
||||||
|
localStorage.setItem('plexsync-username', username);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
setCurrentUser(username);
|
||||||
|
addToast(t('auth.welcome', { user: username }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginError = (msg: string) => {
|
||||||
|
// Toast handles error display, or LoginScreen internal state handles UI
|
||||||
|
addToast(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('plexsync-token');
|
||||||
|
localStorage.removeItem('plexsync-username');
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setCurrentUser('');
|
||||||
|
addToast(t('toasts.loggedOut'));
|
||||||
|
};
|
||||||
|
|
||||||
const getToastStyles = (toast: Toast): React.CSSProperties => {
|
const getToastStyles = (toast: Toast): React.CSSProperties => {
|
||||||
if (toast.exiting || toast.entering) {
|
if (toast.exiting || toast.entering) {
|
||||||
return {
|
return {
|
||||||
@@ -563,6 +600,34 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
const backupInfo = getBackupDisplayInfo(backupSettings);
|
const backupInfo = getBackupDisplayInfo(backupSettings);
|
||||||
|
|
||||||
|
// If not authenticated, show Login Screen
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Render toasts over login screen if needed (e.g. login errors pushed to global toast) */}
|
||||||
|
<div className="fixed top-20 left-0 right-0 flex justify-center h-0 overflow-visible z-[100] pointer-events-none">
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={getToastClasses()}
|
||||||
|
style={getToastStyles(toast)}
|
||||||
|
>
|
||||||
|
<ShieldCheck size={16} />
|
||||||
|
<span>{toast.message}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setToasts(prev => prev.map(t => t.id === toast.id ? { ...t, exiting: true } : t))}
|
||||||
|
className="ml-2 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<LoginScreen onLoginSuccess={handleLoginSuccess} onLoginError={handleLoginError} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
|
|
||||||
@@ -724,6 +789,15 @@ const App: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
|
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Logout Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex items-center justify-center w-9 h-9 rounded-full border border-gray-700 bg-gray-800/50 text-gray-400 hover:text-red-400 hover:border-red-500/30 hover:bg-red-500/10 transition-all"
|
||||||
|
title={t('auth.logout') + ` (${currentUser})`}
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useLanguage } from '../LanguageContext';
|
||||||
|
import { apiService } from '../services/api';
|
||||||
|
import { Lock, User, Loader2, Languages, ArrowRight, ArrowLeftRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface LoginScreenProps {
|
||||||
|
onLoginSuccess: (token: string, username: string) => void;
|
||||||
|
onLoginError: (msg: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginScreen: React.FC<LoginScreenProps> = ({ onLoginSuccess, onLoginError }) => {
|
||||||
|
const { t, language, setLanguage } = useLanguage();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setLocalError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mock credentials: admin / password
|
||||||
|
const response = await apiService.login({ username, password });
|
||||||
|
|
||||||
|
if (response.status === 'success') {
|
||||||
|
onLoginSuccess(response.data.token, response.data.username);
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.message || t('auth.invalidCredentials');
|
||||||
|
setLocalError(errorMsg);
|
||||||
|
onLoginError(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setLocalError(t('auth.invalidCredentials'));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-gray-800 via-gray-900 to-black p-4">
|
||||||
|
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
|
||||||
|
<div className="absolute top-0 left-1/4 w-96 h-96 bg-plex-orange/10 rounded-full blur-[100px] opacity-20"></div>
|
||||||
|
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-[100px] opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language Switcher (Top Right) */}
|
||||||
|
<div className="absolute top-6 right-6 z-20">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLangMenuOpen(!isLangMenuOpen)}
|
||||||
|
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700 hover:border-gray-600 text-gray-300 transition-all backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<Languages size={16} />
|
||||||
|
<span className="text-sm font-medium">{language === 'en' ? 'English' : 'Español'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isLangMenuOpen && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-10" onClick={() => setIsLangMenuOpen(false)}></div>
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-32 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-20 overflow-hidden animate-in fade-in slide-in-from-top-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setLanguage('en'); setIsLangMenuOpen(false); }}
|
||||||
|
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'en' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setLanguage('es'); setIsLangMenuOpen(false); }}
|
||||||
|
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'es' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||||
|
>
|
||||||
|
Español
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Card */}
|
||||||
|
<div className="w-full max-w-md bg-gray-900/60 backdrop-blur-xl border border-gray-700/50 rounded-2xl shadow-2xl p-8 z-10 animate-in zoom-in-95 duration-300">
|
||||||
|
|
||||||
|
<div className="text-center mb-8 flex flex-col items-center">
|
||||||
|
<div className="inline-flex items-center justify-center p-3 rounded-xl bg-gradient-to-br from-plex-orange to-yellow-600 shadow-lg shadow-plex-orange/20 mb-4">
|
||||||
|
<ArrowLeftRight size={32} strokeWidth={2.5} className="text-gray-900" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight text-white">
|
||||||
|
<span className="text-plex-orange">PMS</span> Playlist Sync
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
|
||||||
|
{localError && (
|
||||||
|
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-xs flex items-center justify-center">
|
||||||
|
{localError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">{t('auth.username')}</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User size={16} className="text-gray-500 group-focus-within:text-plex-orange transition-colors" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="w-full h-11 pl-10 pr-4 bg-gray-800/50 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all"
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">{t('auth.password')}</label>
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock size={16} className="text-gray-500 group-focus-within:text-plex-orange transition-colors" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full h-11 pl-10 pr-4 bg-gray-800/50 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !username || !password}
|
||||||
|
className={`w-full h-12 mt-6 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all shadow-lg
|
||||||
|
${isLoading
|
||||||
|
? 'bg-gray-700 text-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-plex-orange text-gray-900 hover:bg-yellow-500 hover:shadow-plex-orange/30 active:scale-[0.98]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
<span>{t('auth.loggingIn')}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{t('auth.loginBtn')}</span>
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-6 border-t border-gray-700/50 text-center">
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
© PMS Playlist Sync
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginScreen;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const en = {
|
export const en = {
|
||||||
app: {
|
app: {
|
||||||
// title and manager are no longer used for branding
|
// title and manager are no longer used for branding
|
||||||
@@ -6,6 +8,17 @@ export const en = {
|
|||||||
manager: 'Manager',
|
manager: 'Manager',
|
||||||
footer: '© {year} PMS Playlist Sync. Connected to Docker backend.',
|
footer: '© {year} PMS Playlist Sync. Connected to Docker backend.',
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
title: 'Login',
|
||||||
|
subtitle: 'Sign in to manage your playlist syncs',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
loginBtn: 'Sign In',
|
||||||
|
logout: 'Logout',
|
||||||
|
loggingIn: 'Verifying...',
|
||||||
|
invalidCredentials: 'Invalid username or password',
|
||||||
|
welcome: 'Welcome, {user}',
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
@@ -143,5 +156,7 @@ export const en = {
|
|||||||
librarySwitched: 'Library switched to {library}',
|
librarySwitched: 'Library switched to {library}',
|
||||||
connectedTo: 'Successfully connected to {name}',
|
connectedTo: 'Successfully connected to {name}',
|
||||||
connectionCancelled: 'Connection cancelled by user.',
|
connectionCancelled: 'Connection cancelled by user.',
|
||||||
|
loginFailed: 'Login failed. Please check credentials.',
|
||||||
|
loggedOut: 'Successfully logged out.',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const es = {
|
export const es = {
|
||||||
app: {
|
app: {
|
||||||
// title and manager are no longer used for branding
|
// title and manager are no longer used for branding
|
||||||
@@ -6,6 +8,17 @@ export const es = {
|
|||||||
manager: 'Gestor',
|
manager: 'Gestor',
|
||||||
footer: '© {year} PMS Playlist Sync. Conectado al backend Docker.',
|
footer: '© {year} PMS Playlist Sync. Conectado al backend Docker.',
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
title: 'Iniciar Sesión',
|
||||||
|
subtitle: 'Ingrese para gestionar sus sincronizaciones',
|
||||||
|
username: 'Usuario',
|
||||||
|
password: 'Password',
|
||||||
|
loginBtn: 'Entrar',
|
||||||
|
logout: 'Salir',
|
||||||
|
loggingIn: 'Verificando...',
|
||||||
|
invalidCredentials: 'Usuario o contraseña incorrectos',
|
||||||
|
welcome: 'Bienvenido, {user}',
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
@@ -143,5 +156,7 @@ export const es = {
|
|||||||
librarySwitched: 'Librería cambiada a {library}',
|
librarySwitched: 'Librería cambiada a {library}',
|
||||||
connectedTo: 'Conectado exitosamente a {name}',
|
connectedTo: 'Conectado exitosamente a {name}',
|
||||||
connectionCancelled: 'Conexión cancelada por usuario.',
|
connectionCancelled: 'Conexión cancelada por usuario.',
|
||||||
|
loginFailed: 'Fallo de inicio de sesión. Verifique credenciales.',
|
||||||
|
loggedOut: 'Sesión cerrada exitosamente.',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, SyncStrategy, PathMappingConfig, ScheduleSettings, ScheduleMode, BackupSettings } from '../types';
|
|
||||||
|
|
||||||
|
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, SyncStrategy, PathMappingConfig, ScheduleSettings, ScheduleMode, BackupSettings, LoginCredentials, AuthResponse } from '../types';
|
||||||
import { MOCK_LOCAL_PLAYLISTS, MOCK_CLOUD_PLAYLISTS } from './mockData';
|
import { MOCK_LOCAL_PLAYLISTS, MOCK_CLOUD_PLAYLISTS } from './mockData';
|
||||||
|
|
||||||
const SIMULATE_DELAY_MS = 800;
|
const SIMULATE_DELAY_MS = 800;
|
||||||
@@ -229,5 +231,27 @@ export const apiService = {
|
|||||||
resolve({ data: null, status: 'success', message: 'Backup settings saved' });
|
resolve({ data: null, status: 'success', message: 'Backup settings saved' });
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mock Login - In a real app this would POST to a backend
|
||||||
|
login: async (creds: LoginCredentials): Promise<ApiResponse<AuthResponse>> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Hardcoded mock credentials for demonstration
|
||||||
|
if (creds.username === 'admin' && creds.password === 'password') {
|
||||||
|
resolve({
|
||||||
|
data: { token: 'mock-jwt-token-123', username: 'admin' },
|
||||||
|
status: 'success',
|
||||||
|
message: 'Login successful'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
data: { token: '', username: '' },
|
||||||
|
status: 'error',
|
||||||
|
message: 'Invalid credentials'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -110,3 +110,13 @@ export interface ApiResponse<T> {
|
|||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoginCredentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user