Sfoglia il codice sorgente

Add save-for-later WiFi option, reorder page, fix playlist preview

WiFi setup:
- Add POST /api/wifi/save to save network without connecting
- Manual entry dialog shows Save and Connect buttons side by side
- Move Networks scan list to bottom of page
- Saved Networks and Hotspot Password cards shown first

Playlist:
- Fix wrong preview when playlist starts with clear pattern
  (removed premature current_playing_file assignment in playlist_manager)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris 4 mesi fa
parent
commit
069e153880

+ 155 - 91
frontend/src/pages/WiFiSetupPage.tsx

@@ -55,6 +55,7 @@ export function WiFiSetupPage() {
   const [showPassword, setShowPassword] = useState(false)
   const [isScanning, setIsScanning] = useState(false)
   const [isConnecting, setIsConnecting] = useState(false)
+  const [isSaving, setIsSaving] = useState(false)
   const [isRebooting, setIsRebooting] = useState(false)
   const [isManualEntry, setIsManualEntry] = useState(false)
   const [manualSsid, setManualSsid] = useState('')
@@ -138,6 +139,29 @@ export function WiFiSetupPage() {
     }
   }
 
+  const handleSave = async () => {
+    const ssid = manualSsid.trim()
+    if (!ssid) return
+
+    setIsSaving(true)
+    try {
+      const result = await apiClient.post<{ success: boolean; message: string }>('/api/wifi/save', {
+        ssid,
+        password: password || '',
+      })
+      if (result.success) {
+        toast.success(result.message)
+        closeDialog()
+        fetchSaved()
+      }
+    } catch (err) {
+      const message = err instanceof Error ? err.message : 'Failed to save network'
+      toast.error(message)
+    } finally {
+      setIsSaving(false)
+    }
+  }
+
   const handleForget = async () => {
     if (!forgetSsid) return
     const ssid = forgetSsid
@@ -181,7 +205,7 @@ export function WiFiSetupPage() {
   }
 
   const closeDialog = () => {
-    if (!isConnecting) {
+    if (!isConnecting && !isSaving) {
       setSelectedNetwork(null)
       setPassword('')
       setShowPassword(false)
@@ -285,79 +309,6 @@ export function WiFiSetupPage() {
         </CardContent>
       </Card>
 
-      {/* Available Networks */}
-      <Card>
-        <CardContent className="pt-4 pb-2 px-4">
-          <div className="flex items-center justify-between mb-2">
-            <div className="flex items-center gap-2">
-              <span className="material-icons-outlined text-base text-muted-foreground">wifi_find</span>
-              <span className="font-semibold text-sm">Networks</span>
-              <span className="text-xs text-muted-foreground">
-                {networks.length > 0
-                  ? `(${networks.length})`
-                  : ''}
-              </span>
-            </div>
-            <div className="flex items-center gap-1">
-              <Button
-                variant="ghost"
-                size="sm"
-                className="h-7 w-7 p-0"
-                onClick={openManualEntry}
-                title="Add network manually"
-              >
-                <span className="material-icons text-base">add</span>
-              </Button>
-              <Button
-                variant="ghost"
-                size="sm"
-                className="h-7 w-7 p-0"
-                onClick={scanNetworks}
-                disabled={isScanning}
-              >
-                <span className={`material-icons text-base ${isScanning ? 'animate-spin' : ''}`}>
-                  refresh
-                </span>
-              </Button>
-            </div>
-          </div>
-          <div className="space-y-0.5">
-            {networks.length === 0 && isScanning && (
-              <p className="text-sm text-muted-foreground text-center py-3">
-                Scanning for networks...
-              </p>
-            )}
-            {networks.length === 0 && !isScanning && (
-              <p className="text-sm text-muted-foreground text-center py-3">
-                No networks found. Try scanning again.
-              </p>
-            )}
-            {networks.map((network) => (
-              <button
-                key={network.ssid}
-                onClick={() => openConnectDialog(network)}
-                className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left transition-colors hover:bg-muted/50
-                  ${network.active ? 'bg-green-50 dark:bg-green-950/20' : ''}`}
-              >
-                <SignalIcon signal={network.signal} />
-                <div className="flex-1 min-w-0">
-                  <p className="font-medium text-sm truncate">{network.ssid}</p>
-                  <p className="text-xs text-muted-foreground">
-                    {network.security}
-                    {network.saved && ' · Saved'}
-                    {network.active && ' · Connected'}
-                  </p>
-                </div>
-                <span className="text-xs text-muted-foreground">{network.signal}%</span>
-                {network.security !== 'Open' && (
-                  <span className="material-icons text-sm text-muted-foreground">lock</span>
-                )}
-              </button>
-            ))}
-          </div>
-        </CardContent>
-      </Card>
-
       {/* Saved Networks */}
       {savedConnections.length > 0 && (
         <Card>
@@ -461,6 +412,79 @@ export function WiFiSetupPage() {
         </>
       )}
 
+      {/* Available Networks */}
+      <Card>
+        <CardContent className="pt-4 pb-2 px-4">
+          <div className="flex items-center justify-between mb-2">
+            <div className="flex items-center gap-2">
+              <span className="material-icons-outlined text-base text-muted-foreground">wifi_find</span>
+              <span className="font-semibold text-sm">Networks</span>
+              <span className="text-xs text-muted-foreground">
+                {networks.length > 0
+                  ? `(${networks.length})`
+                  : ''}
+              </span>
+            </div>
+            <div className="flex items-center gap-1">
+              <Button
+                variant="ghost"
+                size="sm"
+                className="h-7 w-7 p-0"
+                onClick={openManualEntry}
+                title="Add network manually"
+              >
+                <span className="material-icons text-base">add</span>
+              </Button>
+              <Button
+                variant="ghost"
+                size="sm"
+                className="h-7 w-7 p-0"
+                onClick={scanNetworks}
+                disabled={isScanning}
+              >
+                <span className={`material-icons text-base ${isScanning ? 'animate-spin' : ''}`}>
+                  refresh
+                </span>
+              </Button>
+            </div>
+          </div>
+          <div className="space-y-0.5">
+            {networks.length === 0 && isScanning && (
+              <p className="text-sm text-muted-foreground text-center py-3">
+                Scanning for networks...
+              </p>
+            )}
+            {networks.length === 0 && !isScanning && (
+              <p className="text-sm text-muted-foreground text-center py-3">
+                No networks found. Try scanning again.
+              </p>
+            )}
+            {networks.map((network) => (
+              <button
+                key={network.ssid}
+                onClick={() => openConnectDialog(network)}
+                className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left transition-colors hover:bg-muted/50
+                  ${network.active ? 'bg-green-50 dark:bg-green-950/20' : ''}`}
+              >
+                <SignalIcon signal={network.signal} />
+                <div className="flex-1 min-w-0">
+                  <p className="font-medium text-sm truncate">{network.ssid}</p>
+                  <p className="text-xs text-muted-foreground">
+                    {network.security}
+                    {network.saved && ' · Saved'}
+                    {network.active && ' · Connected'}
+                  </p>
+                </div>
+                <span className="text-xs text-muted-foreground">{network.signal}%</span>
+                {network.security !== 'Open' && (
+                  <span className="material-icons text-sm text-muted-foreground">lock</span>
+                )}
+              </button>
+            ))}
+          </div>
+        </CardContent>
+      </Card>
+
       {/* Forget Confirmation Dialog */}
       <Dialog open={forgetSsid !== null} onOpenChange={(open) => { if (!open) setForgetSsid(null) }}>
         <DialogContent className="sm:max-w-md">
@@ -564,23 +588,63 @@ export function WiFiSetupPage() {
               </div>
             )}
 
-            <Button
-              className="w-full"
-              onClick={handleConnect}
-              disabled={isConnecting || (isManualEntry && !manualSsid.trim()) || (!!needsPassword && !password)}
-            >
-              {isConnecting ? (
-                <>
-                  <span className="material-icons text-sm animate-spin mr-2">refresh</span>
-                  Connecting...
-                </>
-              ) : (
-                <>
-                  <span className="material-icons text-sm mr-2">wifi</span>
-                  Connect{isHotspotMode ? ' & Reboot' : ''}
-                </>
-              )}
-            </Button>
+            {isManualEntry ? (
+              <div className="flex gap-2">
+                <Button
+                  variant="outline"
+                  className="flex-1"
+                  onClick={handleSave}
+                  disabled={isSaving || isConnecting || !manualSsid.trim() || (!!needsPassword && !password)}
+                >
+                  {isSaving ? (
+                    <>
+                      <span className="material-icons text-sm animate-spin mr-2">refresh</span>
+                      Saving...
+                    </>
+                  ) : (
+                    <>
+                      <span className="material-icons text-sm mr-2">bookmark_add</span>
+                      Save
+                    </>
+                  )}
+                </Button>
+                <Button
+                  className="flex-1"
+                  onClick={handleConnect}
+                  disabled={isConnecting || isSaving || !manualSsid.trim() || (!!needsPassword && !password)}
+                >
+                  {isConnecting ? (
+                    <>
+                      <span className="material-icons text-sm animate-spin mr-2">refresh</span>
+                      Connecting...
+                    </>
+                  ) : (
+                    <>
+                      <span className="material-icons text-sm mr-2">wifi</span>
+                      Connect{isHotspotMode ? ' & Reboot' : ''}
+                    </>
+                  )}
+                </Button>
+              </div>
+            ) : (
+              <Button
+                className="w-full"
+                onClick={handleConnect}
+                disabled={isConnecting || (!!needsPassword && !password)}
+              >
+                {isConnecting ? (
+                  <>
+                    <span className="material-icons text-sm animate-spin mr-2">refresh</span>
+                    Connecting...
+                  </>
+                ) : (
+                  <>
+                    <span className="material-icons text-sm mr-2">wifi</span>
+                    Connect{isHotspotMode ? ' & Reboot' : ''}
+                  </>
+                )}
+              </Button>
+            )}
           </div>
         </DialogContent>
       </Dialog>

+ 0 - 3
modules/core/playlist_manager.py

@@ -164,9 +164,6 @@ async def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode
         state.current_playlist_name = playlist_name
         state.playlist_mode = run_mode
         state.current_playlist_index = 0
-        # Set current_playing_file to the first pattern as a "preview" - this will be
-        # updated again when actual execution starts, but provides immediate UI feedback.
-        state.current_playing_file = file_paths[0] if file_paths else None
         _current_playlist_task = asyncio.create_task(
             pattern_manager.run_theta_rho_files(
                 file_paths,

+ 48 - 0
modules/wifi/manager.py

@@ -310,6 +310,54 @@ async def connect_to_network(ssid: str, password: str) -> dict:
         return {"success": False, "message": str(e)}
 
 
+def save_network(ssid: str, password: str) -> dict:
+    """Save a WiFi network profile without connecting.
+
+    Creates the connection profile so autohotspot can use it on next check/boot.
+    """
+    try:
+        # Delete any stale connection profile for this SSID (ignore if not found)
+        subprocess.run(
+            ["nmcli", "con", "delete", ssid],
+            capture_output=True, text=True, timeout=10,
+        )
+
+        if password:
+            result = run_nmcli_check(
+                "con", "add",
+                "type", "wifi",
+                "ifname", "wlan0",
+                "con-name", ssid,
+                "ssid", ssid,
+                "wifi-sec.key-mgmt", "wpa-psk",
+                "wifi-sec.psk", password,
+                timeout=15,
+            )
+        else:
+            result = run_nmcli_check(
+                "con", "add",
+                "type", "wifi",
+                "ifname", "wlan0",
+                "con-name", ssid,
+                "ssid", ssid,
+                timeout=15,
+            )
+
+        if result.returncode != 0:
+            error_msg = result.stderr.strip() or "Failed to save network"
+            logger.error(f"WiFi save failed: {error_msg}")
+            return {"success": False, "message": error_msg}
+
+        logger.info(f"Saved WiFi network '{ssid}' (not connecting)")
+        return {"success": True, "message": f"Saved '{ssid}'. Will connect automatically when in range."}
+
+    except subprocess.TimeoutExpired:
+        return {"success": False, "message": "Operation timed out"}
+    except Exception as e:
+        logger.error(f"WiFi save error: {e}")
+        return {"success": False, "message": str(e)}
+
+
 def _schedule_reboot():
     """Trigger a system reboot via systemctl (communicates over D-Bus)."""
     try:

+ 12 - 0
modules/wifi/router.py

@@ -55,6 +55,18 @@ async def wifi_connect(req: WiFiConnectRequest):
     return result
 
 
+@router.post("/save")
+async def wifi_save(req: WiFiConnectRequest):
+    """Save a WiFi network without connecting."""
+    if not req.ssid:
+        raise HTTPException(status_code=400, detail="SSID is required")
+
+    result = manager.save_network(req.ssid, req.password or "")
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["message"])
+    return result
+
+
 @router.post("/forget")
 async def wifi_forget(req: WiFiForgetRequest):
     """Forget a saved WiFi network."""