mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Merge branch '0.2.0b' into main
This commit is contained in:
@@ -177,7 +177,11 @@ async def get_maintenance_types(
|
||||
):
|
||||
"""Get all maintenance types."""
|
||||
await ensure_default_types(db)
|
||||
result = await db.execute(select(MaintenanceType).order_by(MaintenanceType.is_system.desc(), MaintenanceType.name))
|
||||
result = await db.execute(
|
||||
select(MaintenanceType)
|
||||
.where(MaintenanceType.is_deleted.is_(False))
|
||||
.order_by(MaintenanceType.is_system.desc(), MaintenanceType.name)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@@ -230,20 +234,40 @@ async def delete_maintenance_type(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.MAINTENANCE_DELETE),
|
||||
):
|
||||
"""Delete a custom maintenance type."""
|
||||
"""Delete a maintenance type."""
|
||||
result = await db.execute(select(MaintenanceType).where(MaintenanceType.id == type_id))
|
||||
maint_type = result.scalar_one_or_none()
|
||||
if not maint_type:
|
||||
raise HTTPException(status_code=404, detail="Maintenance type not found")
|
||||
|
||||
if maint_type.is_system:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete system maintenance type")
|
||||
maint_type.is_deleted = True
|
||||
await db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
await db.delete(maint_type)
|
||||
await db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.post("/types/restore-defaults")
|
||||
async def restore_default_maintenance_types(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.MAINTENANCE_DELETE),
|
||||
):
|
||||
"""Restore deleted default maintenance types."""
|
||||
await ensure_default_types(db)
|
||||
result = await db.execute(
|
||||
select(MaintenanceType).where(MaintenanceType.is_system.is_(True)).where(MaintenanceType.is_deleted.is_(True))
|
||||
)
|
||||
deleted_types = result.scalars().all()
|
||||
for maint_type in deleted_types:
|
||||
maint_type.is_deleted = False
|
||||
|
||||
await db.commit()
|
||||
return {"restored": len(deleted_types)}
|
||||
|
||||
|
||||
# ============== Printer Maintenance ==============
|
||||
|
||||
|
||||
@@ -264,7 +288,7 @@ async def _get_printer_maintenance_internal(
|
||||
total_hours = await get_printer_total_hours(db, printer_id)
|
||||
|
||||
# Get all maintenance types
|
||||
result = await db.execute(select(MaintenanceType))
|
||||
result = await db.execute(select(MaintenanceType).where(MaintenanceType.is_deleted.is_(False)))
|
||||
all_types = result.scalars().all()
|
||||
|
||||
# Get printer's maintenance items
|
||||
|
||||
@@ -188,6 +188,13 @@ async def run_migrations(conn):
|
||||
# Column already exists
|
||||
pass
|
||||
|
||||
# Migration: Add is_deleted column to maintenance_types for soft-deletes
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE maintenance_types ADD COLUMN is_deleted BOOLEAN DEFAULT 0"))
|
||||
except OperationalError:
|
||||
# Column already exists
|
||||
pass
|
||||
|
||||
# Migration: Add custom_interval_type column to printer_maintenance
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE printer_maintenance ADD COLUMN custom_interval_type VARCHAR(20)"))
|
||||
|
||||
@@ -22,6 +22,7 @@ class MaintenanceType(Base):
|
||||
icon: Mapped[str | None] = mapped_column(String(50)) # Icon name for UI
|
||||
wiki_url: Mapped[str | None] = mapped_column(String(500)) # Documentation link
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False) # Pre-defined vs custom
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) # Hidden/removed type
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -137,7 +137,7 @@ def find_import_shadowing(file_path: Path) -> list[tuple[str, int, str]]:
|
||||
Returns list of (name, line_number, function_name) tuples.
|
||||
"""
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
tree = ast.parse(source)
|
||||
visitor = DangerousImportVisitor()
|
||||
|
||||
@@ -3462,6 +3462,8 @@ export const api = {
|
||||
}),
|
||||
deleteMaintenanceType: (id: number) =>
|
||||
request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
|
||||
restoreDefaultMaintenanceTypes: () =>
|
||||
request<{ restored: number }>(`/maintenance/types/restore-defaults`, { method: 'POST' }),
|
||||
getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
|
||||
getPrinterMaintenance: (printerId: number) =>
|
||||
request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
|
||||
|
||||
@@ -9,6 +9,8 @@ interface ConfirmModalProps {
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
cancelVariant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||
cardClassName?: string;
|
||||
variant?: 'danger' | 'warning' | 'default';
|
||||
isLoading?: boolean;
|
||||
loadingText?: string;
|
||||
@@ -21,6 +23,8 @@ export function ConfirmModal({
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
cancelVariant,
|
||||
cardClassName,
|
||||
variant = 'default',
|
||||
isLoading = false,
|
||||
loadingText,
|
||||
@@ -62,7 +66,10 @@ export function ConfirmModal({
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
||||
onClick={isLoading ? undefined : onCancel}
|
||||
>
|
||||
<Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<Card
|
||||
className={`w-full max-w-md ${cardClassName ?? ''}`}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2 rounded-full bg-bambu-dark ${styles.icon}`}>
|
||||
@@ -74,7 +81,12 @@ export function ConfirmModal({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<Button variant="secondary" onClick={onCancel} className="flex-1" disabled={isLoading}>
|
||||
<Button
|
||||
variant={cancelVariant ?? 'secondary'}
|
||||
onClick={onCancel}
|
||||
className="flex-1"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{resolvedCancelText}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -958,6 +958,7 @@ export default {
|
||||
maintenanceTypes: 'Wartungstypen',
|
||||
maintenanceTypesDescription: 'Systemtypen und Ihre benutzerdefinierten Wartungsaufgaben',
|
||||
addCustomType: 'Benutzerdefinierten Typ hinzufügen',
|
||||
restoreDefaults: 'Standardaufgaben wiederherstellen',
|
||||
intervalType: 'Intervalltyp',
|
||||
intervalValue: 'Intervall ({{type}})',
|
||||
icon: 'Symbol',
|
||||
@@ -1001,11 +1002,14 @@ export default {
|
||||
maintenanceComplete: 'Wartung als abgeschlossen markiert',
|
||||
typeUpdated: 'Wartungstyp aktualisiert',
|
||||
typeDeleted: 'Wartungstyp gelöscht',
|
||||
defaultsRestored: '{{count}} Standardaufgabe(n) wiederhergestellt',
|
||||
printHoursUpdated: 'Druckstunden aktualisiert',
|
||||
printerAssigned: 'Drucker zugewiesen',
|
||||
printerRemoved: 'Drucker entfernt',
|
||||
// Confirmation
|
||||
deleteTypeConfirm: '"{{name}}" löschen?',
|
||||
deleteSystemTypeTitle: 'Standard-Wartungsaufgabe löschen?',
|
||||
deleteSystemTypeMessage: 'Möchten Sie die Standard-Wartungsaufgabe "{{name}}" wirklich löschen?',
|
||||
// Permissions
|
||||
noPermissionUpdate: 'Sie haben keine Berechtigung, Wartungselemente zu aktualisieren',
|
||||
noPermissionPerform: 'Sie haben keine Berechtigung, Wartungen durchzuführen',
|
||||
|
||||
@@ -958,6 +958,7 @@ export default {
|
||||
maintenanceTypes: 'Maintenance Types',
|
||||
maintenanceTypesDescription: 'System types and your custom maintenance tasks',
|
||||
addCustomType: 'Add Custom Type',
|
||||
restoreDefaults: 'Restore Default Tasks',
|
||||
intervalType: 'Interval Type',
|
||||
intervalValue: 'Interval ({{type}})',
|
||||
icon: 'Icon',
|
||||
@@ -1001,11 +1002,14 @@ export default {
|
||||
maintenanceComplete: 'Maintenance marked as complete',
|
||||
typeUpdated: 'Maintenance type updated',
|
||||
typeDeleted: 'Maintenance type deleted',
|
||||
defaultsRestored: 'Restored {{count}} default task(s)',
|
||||
printHoursUpdated: 'Print hours updated',
|
||||
printerAssigned: 'Printer assigned',
|
||||
printerRemoved: 'Printer removed',
|
||||
// Confirmation
|
||||
deleteTypeConfirm: 'Delete "{{name}}"?',
|
||||
deleteSystemTypeTitle: 'Delete default maintenance task?',
|
||||
deleteSystemTypeMessage: 'Are you sure you want to delete the default maintenance task "{{name}}"?',
|
||||
// Permissions
|
||||
noPermissionUpdate: 'You do not have permission to update maintenance items',
|
||||
noPermissionPerform: 'You do not have permission to perform maintenance',
|
||||
|
||||
@@ -945,6 +945,7 @@ export default {
|
||||
maintenanceTypes: 'Tipi di manutenzione',
|
||||
maintenanceTypesDescription: 'Tipi di sistema e tue attivita personalizzate',
|
||||
addCustomType: 'Aggiungi tipo personalizzato',
|
||||
restoreDefaults: 'Ripristina attivita predefinite',
|
||||
intervalType: 'Tipo intervallo',
|
||||
intervalValue: 'Intervallo ({{type}})',
|
||||
icon: 'Icona',
|
||||
@@ -988,11 +989,14 @@ export default {
|
||||
maintenanceComplete: 'Manutenzione segnata come completata',
|
||||
typeUpdated: 'Tipo manutenzione aggiornato',
|
||||
typeDeleted: 'Tipo manutenzione eliminato',
|
||||
defaultsRestored: 'Ripristinate {{count}} attivita predefinite',
|
||||
printHoursUpdated: 'Ore di stampa aggiornate',
|
||||
printerAssigned: 'Stampante assegnata',
|
||||
printerRemoved: 'Stampante rimossa',
|
||||
// Confirmation
|
||||
deleteTypeConfirm: 'Eliminare "{{name}}"?',
|
||||
deleteSystemTypeTitle: 'Eliminare attività di manutenzione predefinita?',
|
||||
deleteSystemTypeMessage: 'Sei sicuro di voler eliminare l\'attività di manutenzione predefinita "{{name}}"?',
|
||||
// Permissions
|
||||
noPermissionUpdate: 'Non hai il permesso di aggiornare elementi manutenzione',
|
||||
noPermissionPerform: 'Non hai il permesso di eseguire manutenzione',
|
||||
|
||||
@@ -1001,6 +1001,7 @@ export default {
|
||||
months: '{{count}}ヶ月',
|
||||
maintenanceTypes: 'メンテナンスタイプ',
|
||||
addCustomType: 'カスタムタイプを追加',
|
||||
restoreDefaults: 'デフォルトタスクを復元',
|
||||
intervalType: 'インターバルタイプ',
|
||||
icon: 'アイコン',
|
||||
documentationLink: 'ドキュメントリンク(任意)',
|
||||
@@ -1034,9 +1035,12 @@ export default {
|
||||
},
|
||||
typeUpdated: 'メンテナンスタイプを更新しました',
|
||||
typeDeleted: 'メンテナンスタイプを削除しました',
|
||||
defaultsRestored: 'デフォルトタスクを{{count}}件復元しました',
|
||||
printerAssigned: 'プリンターを割り当てました',
|
||||
printerRemoved: 'プリンターを削除しました',
|
||||
deleteTypeConfirm: '「{{name}}」を削除しますか?',
|
||||
deleteSystemTypeTitle: 'デフォルトのメンテナンスタスクを削除しますか?',
|
||||
deleteSystemTypeMessage: 'デフォルトのメンテナンスタスク「{{name}}」を削除してもよろしいですか?',
|
||||
noPermissionUpdate: 'メンテナンス記録を更新する権限がありません',
|
||||
noPermissionPerform: 'メンテナンスを実行する権限がありません',
|
||||
noPermissionEditTypes: 'メンテナンスタイプを編集する権限がありません',
|
||||
|
||||
@@ -41,6 +41,7 @@ import type { MaintenanceStatus, PrinterMaintenanceOverview, MaintenanceType, Pe
|
||||
import { Card, CardContent } from '../components/Card';
|
||||
import { Button } from '../components/Button';
|
||||
import { Toggle } from '../components/Toggle';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
@@ -552,6 +553,8 @@ function SettingsSection({
|
||||
onAddType,
|
||||
onUpdateType,
|
||||
onDeleteType,
|
||||
onRestoreDefaults,
|
||||
isRestoringDefaults,
|
||||
onAssignType,
|
||||
onRemoveItem,
|
||||
hasPermission,
|
||||
@@ -563,6 +566,8 @@ function SettingsSection({
|
||||
onAddType: (data: { name: string; description?: string; default_interval_hours: number; interval_type: 'hours' | 'days'; icon?: string; wiki_url?: string | null }, printerIds: number[]) => void;
|
||||
onUpdateType: (id: number, data: { name?: string; default_interval_hours?: number; interval_type?: 'hours' | 'days'; icon?: string; wiki_url?: string | null }) => void;
|
||||
onDeleteType: (id: number) => void;
|
||||
onRestoreDefaults: () => void;
|
||||
isRestoringDefaults: boolean;
|
||||
onAssignType: (printerId: number, typeId: number) => void;
|
||||
onRemoveItem: (itemId: number) => void;
|
||||
hasPermission: (permission: Permission) => boolean;
|
||||
@@ -579,6 +584,7 @@ function SettingsSection({
|
||||
const [newTypeWikiUrl, setNewTypeWikiUrl] = useState('');
|
||||
const [selectedPrinters, setSelectedPrinters] = useState<Set<number>>(new Set());
|
||||
const [expandedType, setExpandedType] = useState<number | null>(null);
|
||||
const [pendingSystemDelete, setPendingSystemDelete] = useState<MaintenanceType | null>(null);
|
||||
|
||||
// Get unique printers from overview
|
||||
const printers = useMemo(() => {
|
||||
@@ -697,14 +703,24 @@ function SettingsSection({
|
||||
<h2 className="text-lg font-semibold text-white">{t('maintenance.maintenanceTypes')}</h2>
|
||||
<p className="text-sm text-bambu-gray mt-1">{t('maintenance.maintenanceTypesDescription')}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowAddType(!showAddType)}
|
||||
disabled={!hasPermission('maintenance:create')}
|
||||
title={!hasPermission('maintenance:create') ? t('maintenance.noPermissionEditTypes') : undefined}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('maintenance.addCustomType')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onRestoreDefaults}
|
||||
disabled={!hasPermission('maintenance:delete') || isRestoringDefaults}
|
||||
title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
|
||||
>
|
||||
{t('maintenance.restoreDefaults')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowAddType(!showAddType)}
|
||||
disabled={!hasPermission('maintenance:create')}
|
||||
title={!hasPermission('maintenance:create') ? t('maintenance.noPermissionEditTypes') : undefined}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('maintenance.addCustomType')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add custom type form */}
|
||||
@@ -846,6 +862,17 @@ function SettingsSection({
|
||||
{formatIntervalLabel(type.default_interval_hours, intervalType, t)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!hasPermission('maintenance:delete')) return;
|
||||
setPendingSystemDelete(type);
|
||||
}}
|
||||
disabled={!hasPermission('maintenance:delete')}
|
||||
title={!hasPermission('maintenance:delete') ? t('maintenance.noPermissionDeleteTypes') : undefined}
|
||||
className={`p-2 rounded-lg hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors ${!hasPermission('maintenance:delete') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1124,6 +1151,23 @@ function SettingsSection({
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{pendingSystemDelete && (
|
||||
<ConfirmModal
|
||||
title={t('maintenance.deleteSystemTypeTitle')}
|
||||
message={t('maintenance.deleteSystemTypeMessage', { name: pendingSystemDelete.name })}
|
||||
confirmText={t('common.delete')}
|
||||
cancelText={t('common.cancel')}
|
||||
variant="danger"
|
||||
cancelVariant="primary"
|
||||
cardClassName="bg-red-950/70 border border-red-800/70"
|
||||
onConfirm={() => {
|
||||
onDeleteType(pendingSystemDelete.id);
|
||||
setPendingSystemDelete(null);
|
||||
}}
|
||||
onCancel={() => setPendingSystemDelete(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1199,6 +1243,18 @@ export function MaintenancePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const restoreDefaultsMutation = useMutation({
|
||||
mutationFn: api.restoreDefaultMaintenanceTypes,
|
||||
onSuccess: (data: { restored: number }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['maintenanceTypes'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] });
|
||||
showToast(t('maintenance.defaultsRestored', { count: data.restored }));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
showToast(error.message, 'error');
|
||||
},
|
||||
});
|
||||
|
||||
const setHoursMutation = useMutation({
|
||||
mutationFn: ({ printerId, hours }: { printerId: number; hours: number }) =>
|
||||
api.setPrinterHours(printerId, hours),
|
||||
@@ -1352,6 +1408,8 @@ export function MaintenancePage() {
|
||||
}}
|
||||
onUpdateType={(id, data) => updateTypeMutation.mutate({ id, data })}
|
||||
onDeleteType={(id) => deleteTypeMutation.mutate(id)}
|
||||
onRestoreDefaults={() => restoreDefaultsMutation.mutate()}
|
||||
isRestoringDefaults={restoreDefaultsMutation.isPending}
|
||||
onAssignType={(printerId, typeId) => assignTypeMutation.mutate({ printerId, typeId })}
|
||||
onRemoveItem={(itemId) => removeItemMutation.mutate(itemId)}
|
||||
hasPermission={hasPermission}
|
||||
|
||||
@@ -636,10 +636,11 @@ function NozzleSlotHoverCard({ slot, index, activeStatus, filamentName, children
|
||||
}
|
||||
|
||||
// Dual-nozzle hover card showing L and R nozzle details side by side
|
||||
function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
|
||||
function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, filamentInfo, children }: {
|
||||
leftSlot?: import('../api/client').NozzleRackSlot;
|
||||
rightSlot?: import('../api/client').NozzleRackSlot;
|
||||
activeNozzle: 'L' | 'R';
|
||||
filamentInfo?: Record<string, { name: string; k: number | null }>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -684,6 +685,8 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
|
||||
const isActive = activeNozzle === side;
|
||||
const typeFull = nozzleTypeName(slot.nozzle_type, t);
|
||||
const flowFull = nozzleFlowName(slot.nozzle_type, t);
|
||||
const filamentCss = parseFilamentColor(slot.filament_color);
|
||||
const filamentName = slot.filament_id ? filamentInfo?.[slot.filament_id]?.name : undefined;
|
||||
return (
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className={`text-[10px] font-bold pb-1 border-b border-bambu-dark-tertiary/50 ${isActive ? 'text-amber-400' : 'text-bambu-gray'}`}>
|
||||
@@ -736,6 +739,19 @@ function DualNozzleHoverCard({ leftSlot, rightSlot, activeNozzle, children }: {
|
||||
<span className="text-[10px] text-white font-mono">{slot.serial_number}</span>
|
||||
</div>
|
||||
)}
|
||||
{(filamentCss || slot.filament_type || slot.filament_id) && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-bambu-gray">{t('printers.nozzleFilament')}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{filamentCss && (
|
||||
<div className="w-3 h-3 rounded-sm border border-white/20" style={{ backgroundColor: filamentCss }} />
|
||||
)}
|
||||
<span className="text-[10px] text-white font-semibold truncate max-w-[100px]">
|
||||
{filamentName || slot.filament_type || slot.filament_id || ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2508,7 +2524,12 @@ function PrinterCard({
|
||||
)}
|
||||
{/* Active nozzle indicator for dual-nozzle printers */}
|
||||
{isDualNozzle && (
|
||||
<DualNozzleHoverCard leftSlot={leftNozzleSlot} rightSlot={rightNozzleSlot} activeNozzle={activeNozzle}>
|
||||
<DualNozzleHoverCard
|
||||
leftSlot={leftNozzleSlot}
|
||||
rightSlot={rightNozzleSlot}
|
||||
activeNozzle={activeNozzle}
|
||||
filamentInfo={filamentInfo}
|
||||
>
|
||||
<div className="text-center px-3 py-1.5 bg-bambu-dark rounded-lg h-full flex flex-col justify-center items-center cursor-default" title={t('printers.activeNozzle', { nozzle: activeNozzle === 'L' ? t('common.left') : t('common.right') })}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`text-[11px] font-bold ${activeNozzle === 'L' ? 'text-amber-400' : 'text-gray-500'}`}>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DIy2OgxD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DMk3iz3Q.css">
|
||||
<script type="module" crossorigin src="/assets/index-IFV5nOKo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DZOdnZuT.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user