Просмотр исходного кода

Added tabed design and auto-save to settings page.

maziggy 5 месяцев назад
Родитель
Сommit
75776fe509

+ 245 - 164
frontend/src/pages/SettingsPage.tsx

@@ -1,5 +1,5 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer } from 'lucide-react';
+import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { AppSettings, SmartPlug, NotificationProvider, UpdateStatus } from '../api/client';
 import type { AppSettings, SmartPlug, NotificationProvider, UpdateStatus } from '../api/client';
@@ -12,19 +12,20 @@ import { AddNotificationModal } from '../components/AddNotificationModal';
 import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { SpoolmanSettings } from '../components/SpoolmanSettings';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
 import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout';
 import { availableLanguages } from '../i18n';
 import { availableLanguages } from '../i18n';
-import { useState, useEffect } from 'react';
+import { useToast } from '../contexts/ToastContext';
+import { useState, useEffect, useRef, useCallback } from 'react';
 
 
 export function SettingsPage() {
 export function SettingsPage() {
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { t, i18n } = useTranslation();
   const { t, i18n } = useTranslation();
+  const { showToast } = useToast();
   const [localSettings, setLocalSettings] = useState<AppSettings | null>(null);
   const [localSettings, setLocalSettings] = useState<AppSettings | null>(null);
-  const [hasChanges, setHasChanges] = useState(false);
-  const [showSaved, setShowSaved] = useState(false);
   const [showPlugModal, setShowPlugModal] = useState(false);
   const [showPlugModal, setShowPlugModal] = useState(false);
   const [editingPlug, setEditingPlug] = useState<SmartPlug | null>(null);
   const [editingPlug, setEditingPlug] = useState<SmartPlug | null>(null);
   const [showNotificationModal, setShowNotificationModal] = useState(false);
   const [showNotificationModal, setShowNotificationModal] = useState(false);
   const [editingProvider, setEditingProvider] = useState<NotificationProvider | null>(null);
   const [editingProvider, setEditingProvider] = useState<NotificationProvider | null>(null);
   const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
   const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
+  const [activeTab, setActiveTab] = useState<'general' | 'plugs' | 'notifications'>('general');
 
 
   const handleDefaultViewChange = (path: string) => {
   const handleDefaultViewChange = (path: string) => {
     setDefaultViewState(path);
     setDefaultViewState(path);
@@ -87,31 +88,18 @@ export function SettingsPage() {
     },
     },
   });
   });
 
 
+  // Ref for debounce timeout
+  const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const isInitialLoadRef = useRef(true);
+
   // Sync local state when settings load
   // Sync local state when settings load
   useEffect(() => {
   useEffect(() => {
     if (settings && !localSettings) {
     if (settings && !localSettings) {
       setLocalSettings(settings);
       setLocalSettings(settings);
-    }
-  }, [settings, localSettings]);
-
-  // Track changes
-  useEffect(() => {
-    if (settings && localSettings) {
-      const changed =
-        settings.auto_archive !== localSettings.auto_archive ||
-        settings.save_thumbnails !== localSettings.save_thumbnails ||
-        settings.capture_finish_photo !== localSettings.capture_finish_photo ||
-        settings.default_filament_cost !== localSettings.default_filament_cost ||
-        settings.currency !== localSettings.currency ||
-        settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
-        settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
-        settings.check_updates !== localSettings.check_updates ||
-        settings.notification_language !== localSettings.notification_language ||
-        settings.ams_humidity_good !== localSettings.ams_humidity_good ||
-        settings.ams_humidity_fair !== localSettings.ams_humidity_fair ||
-        settings.ams_temp_good !== localSettings.ams_temp_good ||
-        settings.ams_temp_fair !== localSettings.ams_temp_fair;
-      setHasChanges(changed);
+      // Mark initial load complete after a short delay
+      setTimeout(() => {
+        isInitialLoadRef.current = false;
+      }, 100);
     }
     }
   }, [settings, localSettings]);
   }, [settings, localSettings]);
 
 
@@ -119,26 +107,63 @@ export function SettingsPage() {
     mutationFn: api.updateSettings,
     mutationFn: api.updateSettings,
     onSuccess: (data) => {
     onSuccess: (data) => {
       queryClient.setQueryData(['settings'], data);
       queryClient.setQueryData(['settings'], data);
-      setLocalSettings(data);
-      setHasChanges(false);
-      setShowSaved(true);
-      setTimeout(() => setShowSaved(false), 2000);
       // Invalidate archive stats to reflect energy tracking mode change
       // Invalidate archive stats to reflect energy tracking mode change
       queryClient.invalidateQueries({ queryKey: ['archiveStats'] });
       queryClient.invalidateQueries({ queryKey: ['archiveStats'] });
+      showToast('Settings saved', 'success');
+    },
+    onError: (error: Error) => {
+      showToast(`Failed to save: ${error.message}`, 'error');
     },
     },
   });
   });
 
 
-  const handleSave = () => {
-    if (localSettings) {
-      updateMutation.mutate(localSettings);
+  // Debounced auto-save when localSettings change
+  useEffect(() => {
+    // Skip if initial load or no settings
+    if (isInitialLoadRef.current || !localSettings || !settings) {
+      return;
     }
     }
-  };
 
 
-  const updateSetting = <K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
-    if (localSettings) {
-      setLocalSettings({ ...localSettings, [key]: value });
+    // Check if there are actual changes
+    const hasChanges =
+      settings.auto_archive !== localSettings.auto_archive ||
+      settings.save_thumbnails !== localSettings.save_thumbnails ||
+      settings.capture_finish_photo !== localSettings.capture_finish_photo ||
+      settings.default_filament_cost !== localSettings.default_filament_cost ||
+      settings.currency !== localSettings.currency ||
+      settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
+      settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
+      settings.check_updates !== localSettings.check_updates ||
+      settings.notification_language !== localSettings.notification_language ||
+      settings.ams_humidity_good !== localSettings.ams_humidity_good ||
+      settings.ams_humidity_fair !== localSettings.ams_humidity_fair ||
+      settings.ams_temp_good !== localSettings.ams_temp_good ||
+      settings.ams_temp_fair !== localSettings.ams_temp_fair;
+
+    if (!hasChanges) {
+      return;
+    }
+
+    // Clear existing timeout
+    if (saveTimeoutRef.current) {
+      clearTimeout(saveTimeoutRef.current);
     }
     }
-  };
+
+    // Set new debounced save (500ms delay)
+    saveTimeoutRef.current = setTimeout(() => {
+      updateMutation.mutate(localSettings);
+    }, 500);
+
+    // Cleanup on unmount or when localSettings changes again
+    return () => {
+      if (saveTimeoutRef.current) {
+        clearTimeout(saveTimeoutRef.current);
+      }
+    };
+  }, [localSettings, settings, updateMutation]);
+
+  const updateSetting = useCallback(<K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
+    setLocalSettings(prev => prev ? { ...prev, [key]: value } : null);
+  }, []);
 
 
   if (isLoading || !localSettings) {
   if (isLoading || !localSettings) {
     return (
     return (
@@ -150,32 +175,59 @@ export function SettingsPage() {
 
 
   return (
   return (
     <div className="p-8">
     <div className="p-8">
-      <div className="mb-8 flex items-center justify-between">
-        <div>
-          <h1 className="text-2xl font-bold text-white">Settings</h1>
-          <p className="text-bambu-gray">Configure Bambusy</p>
-        </div>
-        <Button
-          onClick={handleSave}
-          disabled={!hasChanges || updateMutation.isPending}
+      <div className="mb-8">
+        <h1 className="text-2xl font-bold text-white">Settings</h1>
+        <p className="text-bambu-gray">Configure Bambusy</p>
+      </div>
+
+      {/* Tab Navigation */}
+      <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary">
+        <button
+          onClick={() => setActiveTab('general')}
+          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
+            activeTab === 'general'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray hover:text-white border-transparent'
+          }`}
         >
         >
-          {updateMutation.isPending ? (
-            <Loader2 className="w-4 h-4 animate-spin" />
-          ) : showSaved ? (
-            <Check className="w-4 h-4" />
-          ) : (
-            <Save className="w-4 h-4" />
+          General
+        </button>
+        <button
+          onClick={() => setActiveTab('plugs')}
+          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
+            activeTab === 'plugs'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray hover:text-white border-transparent'
+          }`}
+        >
+          <Plug className="w-4 h-4" />
+          Smart Plugs
+          {smartPlugs && smartPlugs.length > 0 && (
+            <span className="text-xs bg-bambu-dark-tertiary px-1.5 py-0.5 rounded-full">
+              {smartPlugs.length}
+            </span>
+          )}
+        </button>
+        <button
+          onClick={() => setActiveTab('notifications')}
+          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
+            activeTab === 'notifications'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray hover:text-white border-transparent'
+          }`}
+        >
+          <Bell className="w-4 h-4" />
+          Notifications
+          {notificationProviders && notificationProviders.length > 0 && (
+            <span className="text-xs bg-bambu-dark-tertiary px-1.5 py-0.5 rounded-full">
+              {notificationProviders.length}
+            </span>
           )}
           )}
-          {showSaved ? 'Saved!' : 'Save'}
-        </Button>
+        </button>
       </div>
       </div>
 
 
-      {updateMutation.isError && (
-        <div className="mb-6 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
-          Failed to save settings: {(updateMutation.error as Error).message}
-        </div>
-      )}
-
+      {/* General Tab */}
+      {activeTab === 'general' && (
       <div className="flex gap-8">
       <div className="flex gap-8">
         {/* Left Column - General Settings */}
         {/* Left Column - General Settings */}
         <div className="space-y-6 flex-1 max-w-xl">
         <div className="space-y-6 flex-1 max-w-xl">
@@ -386,7 +438,10 @@ export function SettingsPage() {
               </div>
               </div>
             </CardContent>
             </CardContent>
           </Card>
           </Card>
+        </div>
 
 
+        {/* Second Column - AMS & Spoolman */}
+        <div className="space-y-6 flex-1 max-w-md">
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">AMS Display Thresholds</h2>
               <h2 className="text-lg font-semibold text-white">AMS Display Thresholds</h2>
@@ -489,12 +544,12 @@ export function SettingsPage() {
               </div>
               </div>
             </CardContent>
             </CardContent>
           </Card>
           </Card>
-        </div>
 
 
-        {/* Second Column - Spoolman & Updates */}
-        <div className="space-y-6 flex-1 max-w-md">
           <SpoolmanSettings />
           <SpoolmanSettings />
+        </div>
 
 
+        {/* Third Column - Updates */}
+        <div className="space-y-6 flex-1 max-w-sm">
           <Card>
           <Card>
             <CardHeader>
             <CardHeader>
               <h2 className="text-lg font-semibold text-white">Updates</h2>
               <h2 className="text-lg font-semibold text-white">Updates</h2>
@@ -617,90 +672,103 @@ export function SettingsPage() {
             </CardContent>
             </CardContent>
           </Card>
           </Card>
         </div>
         </div>
+      </div>
+      )}
 
 
-        {/* Third Column - Smart Plugs */}
-        <div className="w-80 flex-shrink-0">
-          <Card>
-            <CardHeader>
-              <div className="flex items-center justify-between">
-                <div className="flex items-center gap-2">
-                  <Plug className="w-5 h-5 text-bambu-green" />
-                  <h2 className="text-lg font-semibold text-white">Smart Plugs</h2>
-                </div>
-                <Button
-                  size="sm"
-                  onClick={() => {
-                    setEditingPlug(null);
+      {/* Smart Plugs Tab */}
+      {activeTab === 'plugs' && (
+        <div className="max-w-4xl">
+          <div className="flex items-center justify-between mb-6">
+            <div>
+              <h2 className="text-lg font-semibold text-white flex items-center gap-2">
+                <Plug className="w-5 h-5 text-bambu-green" />
+                Smart Plugs
+              </h2>
+              <p className="text-sm text-bambu-gray mt-1">
+                Connect Tasmota-based smart plugs to automate power control and track energy usage for your printers.
+              </p>
+            </div>
+            <Button
+              onClick={() => {
+                setEditingPlug(null);
+                setShowPlugModal(true);
+              }}
+            >
+              <Plus className="w-4 h-4" />
+              Add Smart Plug
+            </Button>
+          </div>
+
+          {plugsLoading ? (
+            <div className="flex justify-center py-12">
+              <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
+            </div>
+          ) : smartPlugs && smartPlugs.length > 0 ? (
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+              {smartPlugs.map((plug) => (
+                <SmartPlugCard
+                  key={plug.id}
+                  plug={plug}
+                  onEdit={(p) => {
+                    setEditingPlug(p);
                     setShowPlugModal(true);
                     setShowPlugModal(true);
                   }}
                   }}
-                >
-                  <Plus className="w-4 h-4" />
-                  Add
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent>
-              <p className="text-sm text-bambu-gray mb-4">
-                Connect Tasmota-based smart plugs to automate power control for your printers.
-              </p>
-              {plugsLoading ? (
-                <div className="flex justify-center py-8">
-                  <Loader2 className="w-6 h-6 text-bambu-green animate-spin" />
-                </div>
-              ) : smartPlugs && smartPlugs.length > 0 ? (
-                <div className="space-y-4">
-                  {smartPlugs.map((plug) => (
-                    <SmartPlugCard
-                      key={plug.id}
-                      plug={plug}
-                      onEdit={(p) => {
-                        setEditingPlug(p);
-                        setShowPlugModal(true);
-                      }}
-                    />
-                  ))}
-                </div>
-              ) : (
-                <div className="text-center py-8 text-bambu-gray">
-                  <Plug className="w-12 h-12 mx-auto mb-3 opacity-30" />
-                  <p>No smart plugs configured</p>
-                  <p className="text-sm mt-1">Add a Tasmota plug to get started</p>
+                />
+              ))}
+            </div>
+          ) : (
+            <Card>
+              <CardContent className="py-12">
+                <div className="text-center text-bambu-gray">
+                  <Plug className="w-16 h-16 mx-auto mb-4 opacity-30" />
+                  <p className="text-lg font-medium text-white mb-2">No smart plugs configured</p>
+                  <p className="text-sm mb-4">Add a Tasmota-based smart plug to track energy usage and automate power control.</p>
+                  <Button
+                    onClick={() => {
+                      setEditingPlug(null);
+                      setShowPlugModal(true);
+                    }}
+                  >
+                    <Plus className="w-4 h-4" />
+                    Add Your First Smart Plug
+                  </Button>
                 </div>
                 </div>
-              )}
-            </CardContent>
-          </Card>
+              </CardContent>
+            </Card>
+          )}
         </div>
         </div>
+      )}
 
 
-        {/* Fourth Column - Notifications */}
-        <div className="w-80 flex-shrink-0">
-          <Card>
-            <CardHeader>
-              <div className="flex items-center justify-between">
-                <div className="flex items-center gap-2">
-                  <Bell className="w-5 h-5 text-bambu-green" />
-                  <h2 className="text-lg font-semibold text-white">Notifications</h2>
-                </div>
-                <Button
-                  size="sm"
-                  onClick={() => {
-                    setEditingProvider(null);
-                    setShowNotificationModal(true);
-                  }}
-                >
-                  <Plus className="w-4 h-4" />
-                  Add
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent>
-              <p className="text-sm text-bambu-gray mb-4">
-                Get notified about print events via WhatsApp, Telegram, Email, and more.
+      {/* Notifications Tab */}
+      {activeTab === 'notifications' && (
+        <div className="max-w-4xl">
+          <div className="flex items-center justify-between mb-6">
+            <div>
+              <h2 className="text-lg font-semibold text-white flex items-center gap-2">
+                <Bell className="w-5 h-5 text-bambu-green" />
+                Notifications
+              </h2>
+              <p className="text-sm text-bambu-gray mt-1">
+                Get notified about print events via WhatsApp, Telegram, Email, Discord, and more.
               </p>
               </p>
-
-              {/* Notification Language */}
-              <div className="flex items-center justify-between py-3 border-b border-bambu-dark-tertiary mb-4">
+            </div>
+            <Button
+              onClick={() => {
+                setEditingProvider(null);
+                setShowNotificationModal(true);
+              }}
+            >
+              <Plus className="w-4 h-4" />
+              Add Provider
+            </Button>
+          </div>
+
+          {/* Notification Language Setting */}
+          <Card className="mb-6">
+            <CardContent className="py-4">
+              <div className="flex items-center justify-between">
                 <div>
                 <div>
-                  <p className="text-white">{t('settings.notificationLanguage')}</p>
+                  <p className="text-white font-medium">{t('settings.notificationLanguage')}</p>
                   <p className="text-sm text-bambu-gray">{t('settings.notificationLanguageDescription')}</p>
                   <p className="text-sm text-bambu-gray">{t('settings.notificationLanguageDescription')}</p>
                 </div>
                 </div>
                 <select
                 <select
@@ -715,35 +783,48 @@ export function SettingsPage() {
                   ))}
                   ))}
                 </select>
                 </select>
               </div>
               </div>
-
-              {providersLoading ? (
-                <div className="flex justify-center py-8">
-                  <Loader2 className="w-6 h-6 text-bambu-green animate-spin" />
-                </div>
-              ) : notificationProviders && notificationProviders.length > 0 ? (
-                <div className="space-y-4">
-                  {notificationProviders.map((provider) => (
-                    <NotificationProviderCard
-                      key={provider.id}
-                      provider={provider}
-                      onEdit={(p) => {
-                        setEditingProvider(p);
-                        setShowNotificationModal(true);
-                      }}
-                    />
-                  ))}
-                </div>
-              ) : (
-                <div className="text-center py-8 text-bambu-gray">
-                  <Bell className="w-12 h-12 mx-auto mb-3 opacity-30" />
-                  <p>No notification providers configured</p>
-                  <p className="text-sm mt-1">Add a provider to get started</p>
-                </div>
-              )}
             </CardContent>
             </CardContent>
           </Card>
           </Card>
+
+          {providersLoading ? (
+            <div className="flex justify-center py-12">
+              <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
+            </div>
+          ) : notificationProviders && notificationProviders.length > 0 ? (
+            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+              {notificationProviders.map((provider) => (
+                <NotificationProviderCard
+                  key={provider.id}
+                  provider={provider}
+                  onEdit={(p) => {
+                    setEditingProvider(p);
+                    setShowNotificationModal(true);
+                  }}
+                />
+              ))}
+            </div>
+          ) : (
+            <Card>
+              <CardContent className="py-12">
+                <div className="text-center text-bambu-gray">
+                  <Bell className="w-16 h-16 mx-auto mb-4 opacity-30" />
+                  <p className="text-lg font-medium text-white mb-2">No notification providers configured</p>
+                  <p className="text-sm mb-4">Add a notification provider to receive alerts about your print jobs.</p>
+                  <Button
+                    onClick={() => {
+                      setEditingProvider(null);
+                      setShowNotificationModal(true);
+                    }}
+                  >
+                    <Plus className="w-4 h-4" />
+                    Add Your First Provider
+                  </Button>
+                </div>
+              </CardContent>
+            </Card>
+          )}
         </div>
         </div>
-      </div>
+      )}
 
 
       {/* Smart Plug Modal */}
       {/* Smart Plug Modal */}
       {showPlugModal && (
       {showPlugModal && (

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-BOLoyzZ9.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-DO3k-CBG.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-ub2z8B-A.css


+ 2 - 2
static/index.html

@@ -7,8 +7,8 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-CvhOt4Al.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-ub2z8B-A.css">
+    <script type="module" crossorigin src="/assets/index-BOLoyzZ9.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-DO3k-CBG.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Некоторые файлы не были показаны из-за большого количества измененных файлов