mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
Changed layout
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Temperatures on the streaming overlay, and a builder for its URL (#1422, reporter @SMAW)** — The overlay at `/overlay/{printer}` draws live print data over a full-screen camera view for OBS, a wall display or any browser source. It could already be tuned — which fields, what size, what frame rate — but only through query parameters documented in the wiki, and temperatures were not among the fields on offer. Both are now addressed. Nozzle, bed and chamber readings join the list, shown with the target while the heater is still climbing and with the target dropped once it is reached, so a settled hotend reads "220°C" rather than "220 / 220°C" for the rest of the print. Both nozzles appear on a dual-nozzle printer. They are drawn whether or not a print is running, since a preheating machine is exactly when they are worth watching, and each reading appears only when the printer genuinely reports it — chamber temperature stays absent on P1 and A1 models, which publish a value with no sensor behind it. And **Settings → API Keys → Streaming Overlay** now builds the URL for you: pick the printer, tick the fields, set size and frame rate, paste in a token if login is enabled, and copy the result, with an optional preview alongside it. The preview stays off until you ask for it so that leaving the settings page open does not hold a viewer on the printer's single camera connection. Making that preview possible needed one narrow change to the security headers: the overlay path now sends `frame-ancestors 'self'` instead of `'none'`, so Bambuddy's own UI can embed it. Every other page still refuses to be framed at all, `'self'` permits a framer only on this same origin, and embedding the overlay from another host — Home Assistant on a different port, say — is unchanged and still requires `TRUSTED_FRAME_ORIGINS`. Temperatures are not in the default field set, so an overlay URL already pasted into a scene looks exactly the same after upgrading. Translated in all locales, wiki updated, covered by backend and frontend tests.
|
||||
- **The external spool can be hidden from the printer card (#1782, reporter @Arn0uDz)** — An external spool holder that never gets used still occupies a full card's width in the **Filaments** row, next to the AMS units that are actually being used. An eye icon at the right-hand end of that row's header now hides it, and clicking it again brings it back, so nothing is lost behind a settings page you would have to remember. The choice is remembered per printer and stored in the browser, like the card size and the offline-printer filter — one machine in a fleet can be tidied up without touching the others, and nothing changes for anyone else using the same Bambuddy. The icon is deliberately absent on a printer with no AMS: there the external spool is the entire filament section, and hiding it would leave an empty row. That guard also covers the case of an AMS being unplugged from a printer whose external spool was hidden earlier — the spool reappears rather than leaving a blank row behind. On the H2D and H2S both external positions share one card and so hide together. Translated in all locales, wiki updated, covered by frontend tests.
|
||||
|
||||
- **The slice dialog can edit the full print-parameter set, not just pick a preset** — Slicing from Bambuddy meant taking a process preset exactly as it came. Anything beyond that — one more wall for a bracket, supports for a single overhang, slower outer walls on a part that keeps scarring — meant going back to Bambu Studio, editing there, and re-exporting. The slice dialog now has a **Process settings** section carrying the whole tree: the same pages, groups and ordering the desktop slicer shows under Print Settings, with the same labels, tooltips, ranges and defaults, because they are extracted from the slicer's own sources rather than hand-picked. It behaves the way the desktop one does. **Simple / Advanced / Expert** matches the slicer's own visibility tiers, search reaches across every page at once, changed settings are marked and individually revertable, and settings the slicer itself disables in your current configuration are greyed out — infill options with infill at zero, ironing options with ironing off — because Bambuddy evaluates the slicer's own enable rules rather than approximating them. Where a rule cannot be decided with certainty the setting stays editable, on the grounds that a missing control looks like a bug while a redundant one is merely ignored. Edits apply to one slice, are not saved into a preset, and are written after the source file's support configuration and any carried designer settings, so an explicit choice is never silently overridden; an untouched panel produces exactly the request it did before. Parameter names and descriptions are in English even where the rest of Bambuddy is not — several hundred strings lifted verbatim from the slicer, which is a separate job from translating Bambuddy's own interface. The dialog's own wording is translated in all locales. Wiki updated, covered by backend and frontend tests.
|
||||
- **The slice dialog can edit the full print-parameter set, not just pick a preset** — Slicing from Bambuddy meant taking a process preset exactly as it came. Anything beyond that — one more wall for a bracket, supports for a single overhang, slower outer walls on a part that keeps scarring — meant going back to Bambu Studio, editing there, and re-exporting. The slice dialog now has a **Process settings** section carrying the whole tree: the same pages, groups and ordering the desktop slicer shows under Print Settings, with the same labels, tooltips, ranges and defaults, because they are extracted from the slicer's own sources rather than hand-picked. The dialog itself widens to make room: on a reasonably sized screen it now uses two columns, with every "what am I slicing with" decision — pipeline, printer, process, filaments, bed type, layout passes — kept together on the left and the settings panel given a column of its own on the right, open and ready rather than folded away. Narrower screens keep the single column and the collapsed panel. It behaves the way the desktop one does. **Simple / Advanced / Expert** matches the slicer's own visibility tiers, search reaches across every page at once, changed settings are marked and individually revertable, and settings the slicer itself disables in your current configuration are greyed out — infill options with infill at zero, ironing options with ironing off — because Bambuddy evaluates the slicer's own enable rules rather than approximating them. Where a rule cannot be decided with certainty the setting stays editable, on the grounds that a missing control looks like a bug while a redundant one is merely ignored. Edits apply to one slice, are not saved into a preset, and are written after the source file's support configuration and any carried designer settings, so an explicit choice is never silently overridden; an untouched panel produces exactly the request it did before. Parameter names and descriptions are in English even where the rest of Bambuddy is not — several hundred strings lifted verbatim from the slicer, which is a separate job from translating Bambuddy's own interface. The dialog's own wording is translated in all locales. Wiki updated, covered by backend and frontend tests.
|
||||
|
||||
### Changed
|
||||
- **The MQTT debug log now records the commands sent to a printer, not only what it reports back** — **Printer → Debug → MQTT** captured one side of the conversation. Bambuddy listens on both of a printer's topics, but the one carrying commands returned before anything was written to the log, so a capture could show every status push the printer made and nothing it was ever told — including the commands Bambu Studio sends over the local network, which is the only place they can be observed at all. Those now appear alongside Bambuddy's own, grouped under the outgoing filter. It is what lets a question like "which value does Studio put in this field?" be answered from a user's capture instead of guessed at, and it is why #2774 could not be taken further. Commands Bambuddy sends appear twice, once as it publishes and once as the broker echoes it back, and the pair is itself evidence the command reached the broker. Logging is off until switched on, as before. Covered by backend tests.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* the tracker — not here.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '../utils';
|
||||
@@ -1517,6 +1517,72 @@ describe('SliceModal', () => {
|
||||
|
||||
// Pure-function tests for the filament slot picker. Pinned as a separate
|
||||
// describe so the contract is visible without needing the modal mount.
|
||||
/**
|
||||
* The slice dialog switches to a two-column layout once there is room for it,
|
||||
* and the process-settings panel then owns the right-hand column. The global
|
||||
* test setup pins matchMedia to `matches: false`, so every other test in this
|
||||
* file exercises the narrow single-stack path; these override it.
|
||||
*/
|
||||
describe('SliceModal — process settings layout', () => {
|
||||
const setViewport = (wide: boolean) => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: wide,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => true,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockApi.getSlicerPresets.mockResolvedValue(fullThreeTier);
|
||||
mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
|
||||
mockApi.getLibraryFilePlates.mockResolvedValue({
|
||||
file_id: 100,
|
||||
filename: 'Cube.stl',
|
||||
plates: [],
|
||||
is_multi_plate: false,
|
||||
});
|
||||
mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({
|
||||
file_id: 100,
|
||||
filename: 'Cube.stl',
|
||||
plate_id: 1,
|
||||
filaments: [],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => setViewport(false));
|
||||
|
||||
it('keeps the panel collapsed behind a disclosure in the narrow layout', async () => {
|
||||
setViewport(false);
|
||||
renderWithTracker({ source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' }, onClose: vi.fn() });
|
||||
|
||||
const header = await screen.findByRole('button', { name: /Process settings/ });
|
||||
expect(header).toBeEnabled();
|
||||
expect(screen.queryByPlaceholderText('Search settings')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.setup().click(header);
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Search settings')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('opens the panel without a click once it has a column of its own', async () => {
|
||||
setViewport(true);
|
||||
renderWithTracker({ source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' }, onClose: vi.fn() });
|
||||
|
||||
// No disclosure to operate: the panel is the column, so its header is
|
||||
// inert rather than offering to collapse something that has room.
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Search settings')).toBeInTheDocument());
|
||||
expect(screen.getByRole('button', { name: /Process settings/ })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickFilamentForSlot — printer-compat contract (#1851)', () => {
|
||||
// Index that recognises @BBL H2C / @BBL A1 tokens via the canonical
|
||||
// PRINTER_MODEL_MAP. Real production data comes through
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../api/client';
|
||||
import { useSliceJobTracker } from '../contexts/SliceJobTrackerContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useIsWideLayout } from '../hooks/useIsWideLayout';
|
||||
import { PlatePickerModal } from './PlatePickerModal';
|
||||
import SlicerSettingsPanel from './SlicerSettingsPanel';
|
||||
import type { DesignOverride, PlateFilament } from '../types/plates';
|
||||
@@ -264,6 +265,12 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
const [processOverrides, setProcessOverrides] = useState<Record<string, SettingValue>>({});
|
||||
const [serializedProcessOverrides, setSerializedProcessOverrides] = useState<Record<string, string | string[]>>({});
|
||||
const [settingsExpanded, setSettingsExpanded] = useState(false);
|
||||
// Wide enough for the two-column layout, where the panel has a column to
|
||||
// itself and so is always open. The disclosure only exists for the narrow
|
||||
// single-stack layout, in which 348 unfolded options would bury the preset
|
||||
// pickers above them.
|
||||
const isWideLayout = useIsWideLayout();
|
||||
const panelOpen = isWideLayout || settingsExpanded;
|
||||
|
||||
// Slicer Pipelines (#1425) — apply a saved preset bundle to all four slots
|
||||
// with one pick, or save the current selection as a new pipeline.
|
||||
@@ -614,7 +621,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-xl max-h-[85vh] flex flex-col rounded-lg bg-bambu-dark-secondary border border-bambu-dark-tertiary/60"
|
||||
className="w-full max-w-xl lg:max-w-5xl max-h-[85vh] flex flex-col rounded-lg bg-bambu-dark-secondary border border-bambu-dark-tertiary/60"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
@@ -685,6 +692,13 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
status === 'ok' (returns null in that case), but the Refresh
|
||||
button stays visible regardless so users can pick up cloud /
|
||||
bundled changes even when sign-in is healthy. */}
|
||||
{/* Two columns once there is room for them. The left keeps the
|
||||
"what am I slicing with" decisions together; the right gives
|
||||
the process-settings panel a column of its own, which is the
|
||||
only way 348 options are comfortable to work through. Below
|
||||
lg both collapse back into the original single stack. */}
|
||||
<div className="lg:grid lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:gap-5 lg:items-start">
|
||||
<div className="space-y-4 min-w-0">
|
||||
{/* Slicer Pipelines (#1425): apply a saved preset bundle to all
|
||||
four slots, or save the current selection as a pipeline.
|
||||
Pipelines are managed in Settings → Workflow → Pipelines. */}
|
||||
@@ -906,46 +920,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Process settings, mirroring OrcaSlicer's own Print Settings
|
||||
tabs. Collapsed by default: the common case is slicing with a
|
||||
preset as-is, and 348 options unfolded would bury the preset
|
||||
pickers above. Hidden entirely in embedded mode, where no
|
||||
process JSON is sent for these to patch. */}
|
||||
{!useEmbedded && (
|
||||
<div className="rounded border border-bambu-dark-tertiary p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSettingsExpanded((v) => !v)}
|
||||
className="flex w-full items-center justify-between gap-2 text-left"
|
||||
>
|
||||
<span className="text-sm text-white">
|
||||
{t('slice.processSettings', 'Process settings')}
|
||||
<span className="block text-xs text-bambu-gray/70">
|
||||
{t('slice.processSettingsHint', "Adjust the picked preset for this slice. Anything you don't touch stays as the preset defines it.")}
|
||||
</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-bambu-gray">
|
||||
{Object.keys(serializedProcessOverrides).length > 0
|
||||
? t('slice.processSettingsChanged', '{{count}} changed', {
|
||||
count: Object.keys(serializedProcessOverrides).length,
|
||||
})
|
||||
: t('slice.processSettingsUnchanged', 'Preset defaults')}
|
||||
</span>
|
||||
</button>
|
||||
{settingsExpanded && (
|
||||
<div className="mt-3 border-t border-bambu-dark-tertiary pt-3">
|
||||
<SlicerSettingsPanel
|
||||
values={processOverrides}
|
||||
onChange={(values, serialized) => {
|
||||
setProcessOverrides(values);
|
||||
setSerializedProcessOverrides(serialized);
|
||||
}}
|
||||
disabled={isEnqueuing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Bed-type override (#1337). Always visible, always enabled.
|
||||
The backend patches curr_bed_type on the resolved process
|
||||
JSON before forwarding to the sidecar. */}
|
||||
@@ -1047,6 +1021,55 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column: the settings panel. It owns this column, so
|
||||
there is nothing to collapse it out of the way of — the
|
||||
disclosure below lg exists only because the single-column
|
||||
stack cannot afford 348 options unfolded. */}
|
||||
<div className="mt-4 lg:mt-0 min-w-0">
|
||||
{/* Process settings, mirroring OrcaSlicer's own Print Settings
|
||||
tabs. Hidden entirely in embedded mode, where no process
|
||||
JSON is sent for these to patch. */}
|
||||
{!useEmbedded && (
|
||||
<div className="rounded border border-bambu-dark-tertiary p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSettingsExpanded((v) => !v)}
|
||||
aria-expanded={panelOpen}
|
||||
disabled={isWideLayout}
|
||||
className="flex w-full items-center justify-between gap-2 text-left lg:cursor-default"
|
||||
>
|
||||
<span className="text-sm text-white">
|
||||
{t('slice.processSettings', 'Process settings')}
|
||||
<span className="block text-xs text-bambu-gray/70">
|
||||
{t('slice.processSettingsHint', "Adjust the picked preset for this slice. Anything you don't touch stays as the preset defines it.")}
|
||||
</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-bambu-gray">
|
||||
{Object.keys(serializedProcessOverrides).length > 0
|
||||
? t('slice.processSettingsChanged', '{{count}} changed', {
|
||||
count: Object.keys(serializedProcessOverrides).length,
|
||||
})
|
||||
: t('slice.processSettingsUnchanged', 'Preset defaults')}
|
||||
</span>
|
||||
</button>
|
||||
{panelOpen && (
|
||||
<div className="mt-3 border-t border-bambu-dark-tertiary pt-3">
|
||||
<SlicerSettingsPanel
|
||||
values={processOverrides}
|
||||
onChange={(values, serialized) => {
|
||||
setProcessOverrides(values);
|
||||
setSerializedProcessOverrides(serialized);
|
||||
}}
|
||||
disabled={isEnqueuing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -213,7 +213,9 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
|
||||
{t('slicerSettings.noMatches', 'No settings match this search.')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 max-h-[22rem] overflow-y-auto pr-1">
|
||||
// Taller once the panel has a column of its own; the narrow cap keeps
|
||||
// it from swallowing the single-column stack on small screens.
|
||||
<div className="flex flex-col gap-4 max-h-[22rem] lg:max-h-[58vh] overflow-y-auto pr-1">
|
||||
{shownPages.map((p) => (
|
||||
<div key={p.page} className="flex flex-col gap-3">
|
||||
{query.trim() && <p className="text-[0.7rem] uppercase tracking-wide text-bambu-gray/70">{p.page}</p>}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Tailwind's `lg`. Kept in sync with the `lg:` classes it is paired with —
|
||||
* components using this hook usually also switch layout via `lg:` utilities,
|
||||
* and the two disagreeing produces a half-applied layout.
|
||||
*/
|
||||
const WIDE_LAYOUT_BREAKPOINT = 1024;
|
||||
|
||||
/**
|
||||
* True when there is room for a side-by-side layout.
|
||||
*
|
||||
* Prefer plain `lg:` classes where CSS alone can do the job. This exists for
|
||||
* the cases where the *behaviour* differs rather than only the styling — a
|
||||
* disclosure that collapses on narrow screens but is permanently open when it
|
||||
* has its own column, for instance, which CSS cannot express on its own.
|
||||
*/
|
||||
export function useIsWideLayout(): boolean {
|
||||
const [isWide, setIsWide] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.innerWidth >= WIDE_LAYOUT_BREAKPOINT : false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(`(min-width: ${WIDE_LAYOUT_BREAKPOINT}px)`);
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setIsWide(e.matches);
|
||||
};
|
||||
|
||||
setIsWide(mediaQuery.matches);
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
return isWide;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -26,8 +26,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-Bfjo96N3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DQ9iPYXW.css">
|
||||
<script type="module" crossorigin src="/assets/index-D5dXOgnd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DcBH50JZ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user