Squashed 'sample-front-end/' changes from 552f9c4..99ea3a6
99ea3a6 feat: Display next sync schedule information fb8d17a feat: Implement schedule settings and basic UI git-subtree-dir: sample-front-end git-subtree-split: 99ea3a68de98503b706d3ee5782baf4a66dc7134
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, RegexReplacement, SyncState } from './types';
|
||||
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, RegexReplacement, SyncState, ScheduleSettings, ScheduleMode } from './types';
|
||||
import { apiService } from './services/api';
|
||||
import {
|
||||
STRIPE_BASE_SPEED,
|
||||
@@ -15,7 +17,7 @@ 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 } from 'lucide-react';
|
||||
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock } from 'lucide-react';
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
@@ -31,7 +33,7 @@ const useStripeAnimation = (syncState: SyncState) => {
|
||||
const rightYellowRef = useRef<HTMLDivElement>(null);
|
||||
const rightGreenRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const requestRef = useRef<number>();
|
||||
const requestRef = useRef<number | undefined>(undefined);
|
||||
const lastTimeRef = useRef<number>(0);
|
||||
const offsetRef = useRef<number>(0);
|
||||
|
||||
@@ -138,6 +140,16 @@ const App: React.FC = () => {
|
||||
// Regex State
|
||||
const [regexReplacements, setRegexReplacements] = useState<RegexReplacement[]>([]);
|
||||
|
||||
// Schedule State
|
||||
const [scheduleSettings, setScheduleSettings] = useState<ScheduleSettings>({
|
||||
mode: ScheduleMode.DISABLED,
|
||||
cronExpression: '',
|
||||
dailyTime: '02:00',
|
||||
weeklyDays: [0], // Sunday
|
||||
weeklyTime: '03:00',
|
||||
autoWatch: false
|
||||
});
|
||||
|
||||
// Toast Notification System
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
const timeoutsRef = useRef<{[key: number]: ReturnType<typeof setTimeout>}>({});
|
||||
@@ -286,6 +298,29 @@ const App: React.FC = () => {
|
||||
addToast('Regex preprocessing rules have been saved.');
|
||||
};
|
||||
|
||||
// Handle Schedule Save
|
||||
const handleSaveSchedule = async (settings: ScheduleSettings): Promise<boolean> => {
|
||||
// Call API (validation happens in Mock)
|
||||
const result = await apiService.saveScheduleSettings(settings);
|
||||
|
||||
if (result.status === 'success') {
|
||||
// Only update local state if successful
|
||||
setScheduleSettings(settings);
|
||||
|
||||
if (settings.mode === ScheduleMode.DISABLED) {
|
||||
addToast("Scheduled tasks disabled.");
|
||||
} else if (settings.mode === ScheduleMode.CRON && settings.cronExpression.trim() === '') {
|
||||
addToast("Scheduled tasks disabled (Empty Cron).");
|
||||
} else {
|
||||
addToast("Scheduled task started successfully.");
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
addToast(result.message || "Failed to update schedule.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Sync Trigger
|
||||
const handleSyncTrigger = async () => {
|
||||
if (syncState !== SyncState.IDLE) return;
|
||||
@@ -346,6 +381,84 @@ const App: React.FC = () => {
|
||||
|
||||
const isConnected = cloudServerInfo?.isConnected;
|
||||
|
||||
// Helper: Calculate Next Run Info
|
||||
const getScheduleDisplayInfo = (settings: ScheduleSettings) => {
|
||||
if (settings.mode === ScheduleMode.DISABLED) {
|
||||
return { label: 'Auto-Sync', value: 'Disabled', active: false };
|
||||
}
|
||||
|
||||
if (settings.mode === ScheduleMode.CRON) {
|
||||
return { label: 'Cron Schedule', value: settings.cronExpression || 'Pending...', active: true };
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
let nextRun: Date | null = null;
|
||||
let timeStr = '';
|
||||
|
||||
if (settings.mode === ScheduleMode.DAILY) {
|
||||
const [h, m] = settings.dailyTime.split(':').map(Number);
|
||||
const target = new Date();
|
||||
target.setHours(h, m, 0, 0);
|
||||
timeStr = settings.dailyTime;
|
||||
|
||||
if (now < target) {
|
||||
nextRun = target;
|
||||
} else {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(h, m, 0, 0);
|
||||
nextRun = tomorrow;
|
||||
}
|
||||
} else if (settings.mode === ScheduleMode.WEEKLY) {
|
||||
timeStr = settings.weeklyTime;
|
||||
const [h, m] = settings.weeklyTime.split(':').map(Number);
|
||||
const activeDays = [...settings.weeklyDays].sort();
|
||||
|
||||
if (activeDays.length === 0) return { label: 'Weekly Schedule', value: 'No days selected', active: false };
|
||||
|
||||
// Check rest of today
|
||||
if (activeDays.includes(now.getDay())) {
|
||||
const todayTarget = new Date();
|
||||
todayTarget.setHours(h, m, 0, 0);
|
||||
if (todayTarget > now) {
|
||||
nextRun = todayTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// Check future days
|
||||
if (!nextRun) {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
const nextDayIndex = (now.getDay() + i) % 7;
|
||||
if (activeDays.includes(nextDayIndex)) {
|
||||
const d = new Date();
|
||||
d.setDate(now.getDate() + i);
|
||||
d.setHours(h, m, 0, 0);
|
||||
nextRun = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextRun) {
|
||||
// Format logic
|
||||
const isToday = nextRun.getDate() === now.getDate() && nextRun.getMonth() === now.getMonth();
|
||||
const isTomorrow = new Date(now.getTime() + 86400000).getDate() === nextRun.getDate();
|
||||
|
||||
let dateStr = '';
|
||||
if (isToday) dateStr = 'Today';
|
||||
else if (isTomorrow) dateStr = 'Tomorrow';
|
||||
else dateStr = days[nextRun.getDay()];
|
||||
|
||||
return { label: `${settings.mode === ScheduleMode.DAILY ? 'Daily' : 'Weekly'} Schedule`, value: `${dateStr} at ${timeStr}`, active: true };
|
||||
}
|
||||
|
||||
return { label: 'Schedule', value: 'Not configured', active: false };
|
||||
};
|
||||
|
||||
const scheduleInfo = getScheduleDisplayInfo(scheduleSettings);
|
||||
|
||||
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">
|
||||
|
||||
@@ -411,7 +524,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} />
|
||||
@@ -421,18 +534,32 @@ const App: React.FC = () => {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Connection Status Button */}
|
||||
<button
|
||||
onClick={() => setIsConnectionModalOpen(true)}
|
||||
className={`flex items-center justify-center w-9 h-9 rounded-full border transition-all duration-300 hover:scale-105 active:scale-95 shadow-md
|
||||
${isConnected
|
||||
? "bg-green-500/10 border-green-500/50 text-green-400 hover:bg-green-500/20 hover:shadow-green-500/20"
|
||||
: "bg-red-500/10 border-red-500/50 text-red-400 hover:bg-red-500/20 hover:shadow-red-500/20"
|
||||
}`}
|
||||
title={isConnected ? "Connected to Plex" : "Disconnected"}
|
||||
>
|
||||
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
|
||||
</button>
|
||||
{/* Normal Toolbar Right */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 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">
|
||||
{scheduleInfo.label}
|
||||
</span>
|
||||
<div className={`text-xs font-mono flex items-center gap-1.5 ${scheduleInfo.active ? 'text-plex-orange' : 'text-gray-600'}`}>
|
||||
{scheduleInfo.active && <Clock size={12} />}
|
||||
<span>{scheduleInfo.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Status Button */}
|
||||
<button
|
||||
onClick={() => setIsConnectionModalOpen(true)}
|
||||
className={`flex items-center justify-center w-9 h-9 rounded-full border transition-all duration-300 hover:scale-105 active:scale-95 shadow-md
|
||||
${isConnected
|
||||
? "bg-green-500/10 border-green-500/50 text-green-400 hover:bg-green-500/20 hover:shadow-green-500/20"
|
||||
: "bg-red-500/10 border-red-500/50 text-red-400 hover:bg-red-500/20 hover:shadow-red-500/20"
|
||||
}`}
|
||||
title={isConnected ? "Connected to Plex" : "Disconnected"}
|
||||
>
|
||||
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Syncing / Success Text Banner */
|
||||
@@ -504,6 +631,8 @@ const App: React.FC = () => {
|
||||
onSelect={handleStrategyChange}
|
||||
savedRegexReplacements={regexReplacements}
|
||||
onSaveRegex={handleSaveRegex}
|
||||
savedSchedule={scheduleSettings}
|
||||
onSaveSchedule={handleSaveSchedule}
|
||||
syncState={syncState}
|
||||
onSync={handleSyncTrigger}
|
||||
/>
|
||||
@@ -540,4 +669,4 @@ const App: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
Reference in New Issue
Block a user