PlexPlaylist_UI subtree merge
feat: Implement user authentication and login screen Merge commit 'a14210c458d5f6c6a4875ca8228db63c0b73cf75'
This commit is contained in:
+88
-14
@@ -1,4 +1,6 @@
|
||||
|
||||
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from './types';
|
||||
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 StrategySelector from './components/StrategySelector';
|
||||
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';
|
||||
|
||||
interface Toast {
|
||||
@@ -115,6 +118,12 @@ const useStripeAnimation = (syncState: SyncState) => {
|
||||
|
||||
const App: React.FC = () => {
|
||||
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 [cloudPlaylists, setCloudPlaylists] = useState<Playlist[]>([]);
|
||||
const [cloudServerInfo, setCloudServerInfo] = useState<PlexServerConnection | undefined>(undefined);
|
||||
@@ -198,6 +207,16 @@ const App: React.FC = () => {
|
||||
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
|
||||
useEffect(() => {
|
||||
const enteringIds = toasts.filter(t => t.entering).map(t => t.id);
|
||||
@@ -236,6 +255,7 @@ const App: React.FC = () => {
|
||||
|
||||
// Fetch Local Playlists
|
||||
const refreshLocal = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
if (localAbortRef.current) localAbortRef.current.abort();
|
||||
const abortController = new AbortController();
|
||||
localAbortRef.current = abortController;
|
||||
@@ -247,7 +267,7 @@ const App: React.FC = () => {
|
||||
}
|
||||
setLoadingLocal(false);
|
||||
localAbortRef.current = null;
|
||||
}, []);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const cancelLocalRefresh = () => {
|
||||
if (localAbortRef.current) {
|
||||
@@ -260,6 +280,7 @@ const App: React.FC = () => {
|
||||
|
||||
// Fetch Cloud Playlists and Info
|
||||
const refreshCloud = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
||||
const abortController = new AbortController();
|
||||
cloudAbortRef.current = abortController;
|
||||
@@ -281,7 +302,7 @@ const App: React.FC = () => {
|
||||
setLoadingCloud(false);
|
||||
cloudAbortRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const cancelCloudRefresh = () => {
|
||||
if (cloudAbortRef.current) {
|
||||
@@ -292,16 +313,18 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
// Initial Load (Only if Authenticated)
|
||||
useEffect(() => {
|
||||
refreshLocal();
|
||||
refreshCloud();
|
||||
if (isAuthenticated) {
|
||||
refreshLocal();
|
||||
refreshCloud();
|
||||
}
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
if (localAbortRef.current) localAbortRef.current.abort();
|
||||
if (cloudAbortRef.current) cloudAbortRef.current.abort();
|
||||
}
|
||||
}, [refreshLocal, refreshCloud]);
|
||||
}, [isAuthenticated, refreshLocal, refreshCloud]);
|
||||
|
||||
// Handle Strategy Change
|
||||
const handleStrategyChange = (strategy: SyncStrategy, label: string) => {
|
||||
@@ -364,13 +387,6 @@ const App: React.FC = () => {
|
||||
|
||||
// Timing Breakdown:
|
||||
// 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(() => {
|
||||
setSyncState(SyncState.IDLE);
|
||||
refreshLocal();
|
||||
@@ -390,6 +406,27 @@ const App: React.FC = () => {
|
||||
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 => {
|
||||
if (toast.exiting || toast.entering) {
|
||||
return {
|
||||
@@ -563,6 +600,34 @@ const App: React.FC = () => {
|
||||
|
||||
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 (
|
||||
<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} />}
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user