|
@@ -1,7 +1,7 @@
|
|
|
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 } from 'lucide-react';
|
|
|
|
|
|
|
+import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink } from 'lucide-react';
|
|
|
import { api } from '../api/client';
|
|
import { api } from '../api/client';
|
|
|
-import type { AppSettings, SmartPlug, NotificationProvider } from '../api/client';
|
|
|
|
|
|
|
+import type { AppSettings, SmartPlug, NotificationProvider, UpdateStatus } from '../api/client';
|
|
|
import { Card, CardContent, CardHeader } from '../components/Card';
|
|
import { Card, CardContent, CardHeader } from '../components/Card';
|
|
|
import { Button } from '../components/Button';
|
|
import { Button } from '../components/Button';
|
|
|
import { SmartPlugCard } from '../components/SmartPlugCard';
|
|
import { SmartPlugCard } from '../components/SmartPlugCard';
|
|
@@ -53,6 +53,37 @@ export function SettingsPage() {
|
|
|
queryFn: api.checkFfmpeg,
|
|
queryFn: api.checkFfmpeg,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const { data: versionInfo } = useQuery({
|
|
|
|
|
+ queryKey: ['version'],
|
|
|
|
|
+ queryFn: api.getVersion,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const { data: updateCheck, refetch: refetchUpdateCheck, isRefetching: isCheckingUpdate } = useQuery({
|
|
|
|
|
+ queryKey: ['updateCheck'],
|
|
|
|
|
+ queryFn: api.checkForUpdates,
|
|
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const { data: updateStatus, refetch: refetchUpdateStatus } = useQuery({
|
|
|
|
|
+ queryKey: ['updateStatus'],
|
|
|
|
|
+ queryFn: api.getUpdateStatus,
|
|
|
|
|
+ refetchInterval: (query) => {
|
|
|
|
|
+ const status = query.state.data as UpdateStatus | undefined;
|
|
|
|
|
+ // Poll while update is in progress
|
|
|
|
|
+ if (status?.status === 'downloading' || status?.status === 'installing') {
|
|
|
|
|
+ return 1000;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const applyUpdateMutation = useMutation({
|
|
|
|
|
+ mutationFn: api.applyUpdate,
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ refetchUpdateStatus();
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
// Sync local state when settings load
|
|
// Sync local state when settings load
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (settings && !localSettings) {
|
|
if (settings && !localSettings) {
|
|
@@ -70,7 +101,8 @@ export function SettingsPage() {
|
|
|
settings.default_filament_cost !== localSettings.default_filament_cost ||
|
|
settings.default_filament_cost !== localSettings.default_filament_cost ||
|
|
|
settings.currency !== localSettings.currency ||
|
|
settings.currency !== localSettings.currency ||
|
|
|
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
|
|
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
|
|
|
- settings.energy_tracking_mode !== localSettings.energy_tracking_mode;
|
|
|
|
|
|
|
+ settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
|
|
|
|
|
+ settings.check_updates !== localSettings.check_updates;
|
|
|
setHasChanges(changed);
|
|
setHasChanges(changed);
|
|
|
}
|
|
}
|
|
|
}, [settings, localSettings]);
|
|
}, [settings, localSettings]);
|
|
@@ -326,29 +358,137 @@ 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 />
|
|
|
|
|
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
- <h2 className="text-lg font-semibold text-white">About</h2>
|
|
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-white">Updates</h2>
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
- <CardContent>
|
|
|
|
|
- <div className="space-y-2 text-sm">
|
|
|
|
|
- <p className="text-white">Bambusy v0.1.2</p>
|
|
|
|
|
- <p className="text-bambu-gray">
|
|
|
|
|
- Archive and manage your Bambu Lab 3MF files
|
|
|
|
|
- </p>
|
|
|
|
|
- <p className="text-bambu-gray">
|
|
|
|
|
- Connect to printers via LAN mode (developer mode required)
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ <CardContent className="space-y-4">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-white">Check for updates</p>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">
|
|
|
|
|
+ Automatically check for new versions on startup
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <label className="relative inline-flex items-center cursor-pointer">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ checked={localSettings.check_updates}
|
|
|
|
|
+ onChange={(e) => updateSetting('check_updates', e.target.checked)}
|
|
|
|
|
+ className="sr-only peer"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="border-t border-bambu-dark-tertiary pt-4">
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-white">Current version</p>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">v{versionInfo?.version || '...'}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="secondary"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => refetchUpdateCheck()}
|
|
|
|
|
+ disabled={isCheckingUpdate}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isCheckingUpdate ? (
|
|
|
|
|
+ <Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <RefreshCw className="w-4 h-4" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ Check now
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {updateCheck?.update_available ? (
|
|
|
|
|
+ <div className="mt-4 p-3 bg-bambu-green/10 border border-bambu-green/30 rounded-lg">
|
|
|
|
|
+ <div className="flex items-start justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-bambu-green font-medium">
|
|
|
|
|
+ Update available: v{updateCheck.latest_version}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ {updateCheck.release_name && updateCheck.release_name !== updateCheck.latest_version && (
|
|
|
|
|
+ <p className="text-sm text-bambu-gray mt-1">{updateCheck.release_name}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {updateCheck.release_notes && (
|
|
|
|
|
+ <p className="text-sm text-bambu-gray mt-2 whitespace-pre-line line-clamp-3">
|
|
|
|
|
+ {updateCheck.release_notes}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {updateCheck.release_url && (
|
|
|
|
|
+ <a
|
|
|
|
|
+ href={updateCheck.release_url}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ rel="noopener noreferrer"
|
|
|
|
|
+ className="text-bambu-gray hover:text-white transition-colors"
|
|
|
|
|
+ title="View release on GitHub"
|
|
|
|
|
+ >
|
|
|
|
|
+ <ExternalLink className="w-4 h-4" />
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {updateStatus?.status === 'downloading' || updateStatus?.status === 'installing' ? (
|
|
|
|
|
+ <div className="mt-3">
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-bambu-gray">
|
|
|
|
|
+ <Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
+ <span>{updateStatus.message}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="mt-2 w-full bg-bambu-dark-tertiary rounded-full h-2">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="bg-bambu-green h-2 rounded-full transition-all duration-300"
|
|
|
|
|
+ style={{ width: `${updateStatus.progress}%` }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : updateStatus?.status === 'complete' ? (
|
|
|
|
|
+ <div className="mt-3 p-2 bg-bambu-green/20 rounded text-sm text-bambu-green">
|
|
|
|
|
+ {updateStatus.message}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : updateStatus?.status === 'error' ? (
|
|
|
|
|
+ <div className="mt-3 p-2 bg-red-500/20 rounded text-sm text-red-400">
|
|
|
|
|
+ {updateStatus.error || updateStatus.message}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="mt-3"
|
|
|
|
|
+ onClick={() => applyUpdateMutation.mutate()}
|
|
|
|
|
+ disabled={applyUpdateMutation.isPending}
|
|
|
|
|
+ >
|
|
|
|
|
+ {applyUpdateMutation.isPending ? (
|
|
|
|
|
+ <Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Download className="w-4 h-4" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ Install Update
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : updateCheck?.error ? (
|
|
|
|
|
+ <div className="mt-2 p-2 bg-red-500/10 border border-red-500/30 rounded text-sm text-red-400">
|
|
|
|
|
+ Failed to check for updates: {updateCheck.error}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : updateCheck && !updateCheck.update_available ? (
|
|
|
|
|
+ <p className="mt-2 text-sm text-bambu-gray">
|
|
|
|
|
+ You're running the latest version
|
|
|
|
|
+ </p>
|
|
|
|
|
+ ) : null}
|
|
|
</div>
|
|
</div>
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* Middle Column - Smart Plugs */}
|
|
|
|
|
- <div className="w-96 flex-shrink-0">
|
|
|
|
|
|
|
+ {/* Third Column - Smart Plugs */}
|
|
|
|
|
+ <div className="w-80 flex-shrink-0">
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center justify-between">
|
|
@@ -400,8 +540,8 @@ export function SettingsPage() {
|
|
|
</Card>
|
|
</Card>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* Right Column - Notifications */}
|
|
|
|
|
- <div className="w-96 flex-shrink-0">
|
|
|
|
|
|
|
+ {/* Fourth Column - Notifications */}
|
|
|
|
|
+ <div className="w-80 flex-shrink-0">
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center justify-between">
|