feat: adicionar suporte a impressoras pequenas com parâmetro dpi e layout adaptativo para etiquetas
This commit is contained in:
+52
-2
@@ -320,8 +320,8 @@ Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
|
||||
| PATCH | `/filaments/:id` | Atualizar |
|
||||
| DELETE | `/filaments/:id` | Excluir |
|
||||
| GET | `/filaments/:id/qrcode` | QR Code PNG |
|
||||
| GET | `/filaments/:id/label.svg` | Etiqueta SVG (`width_mm`, `height_mm`, `fields`) |
|
||||
| GET | `/filaments/:id/label.pdf` | Etiqueta PDF (`width_mm`, `height_mm`, `fields`) |
|
||||
| GET | `/filaments/:id/label.svg` | Etiqueta SVG (`width_mm`, `height_mm`, `fields`, `dpi`) |
|
||||
| GET | `/filaments/:id/label.pdf` | Etiqueta PDF (`width_mm`, `height_mm`, `fields`, `dpi`) |
|
||||
|
||||
### Spool Presets
|
||||
|
||||
@@ -349,6 +349,7 @@ Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
|
||||
- [x] Exportar etiqueta SVG via `GET /filaments/:id/label.svg`
|
||||
- [x] Exportar etiqueta PDF via `GET /filaments/:id/label.pdf`
|
||||
- [x] Seletor de formato (PDF/SVG) e controle de campos na tela de etiqueta
|
||||
- [x] Suporte a Niimbot D11/D110 (22×14mm) com seleção automática de conteúdo mínimo
|
||||
- [x] NFC leitura NTAG215 → `nfc-reader.tsx` (bottom sheet modal)
|
||||
- [x] NFC gravação NTAG215 → `filaments/[id]/write-nfc.tsx`
|
||||
- [ ] NFC suporte iOS (requer entitlement `com.apple.developer.nfc.readwrite`)
|
||||
@@ -360,6 +361,55 @@ Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
|
||||
|
||||
## Mudanças Recentes (14/03/2026)
|
||||
|
||||
### ✅ Suporte a Impressoras Pequenas — Niimbot D11/D110 (`label.tsx`)
|
||||
|
||||
**Arquivo alterado**: `app/(app)/filaments/[id]/label.tsx`
|
||||
|
||||
#### Preset Niimbot 22×14mm
|
||||
|
||||
Adicionado como primeira opção em `LABEL_SIZES`:
|
||||
|
||||
```ts
|
||||
{ id: '22x14', label: '22 × 14', sub: 'Niimbot D11/D110' }
|
||||
```
|
||||
|
||||
O tipo `LabelSize` foi atualizado para incluir `'22x14'`.
|
||||
|
||||
#### Seleção automática de conteúdo mínimo
|
||||
|
||||
A função `handleSizeChange` substitui o `setSelectedSize` direto nos chips. Ao selecionar `22x14`, o conteúdo ativo é automaticamente reduzido para `{ color, name, qrcode }` — os únicos campos que cabem fisicamente em 22×14mm. O usuário pode ajustar manualmente após.
|
||||
|
||||
```ts
|
||||
const MINI_LABEL_CONTENT: Set<ContentOption> = new Set(['color', 'name', 'qrcode']);
|
||||
|
||||
function handleSizeChange(size: LabelSize): void {
|
||||
setSelectedSize(size);
|
||||
if (size === '22x14') setEnabledContent(new Set(MINI_LABEL_CONTENT));
|
||||
}
|
||||
```
|
||||
|
||||
#### `dpi=203` enviado para o backend
|
||||
|
||||
O parâmetro `dpi` é incluído automaticamente nos requests de `/label.pdf` e `/label.svg` quando Niimbot está selecionado. Instrui o backend a calcular pixels do SVG na resolução correta da impressora térmica.
|
||||
|
||||
#### Fix: btoa chunking para PDFs grandes
|
||||
|
||||
O código original usava `String.fromCharCode(...new Uint8Array(buffer))` com spread, que causa **stack overflow** em buffers > ~64KB em React Native. Substituído por loop em chunks de 8192 bytes:
|
||||
|
||||
```ts
|
||||
const bytes = new Uint8Array(response.data);
|
||||
let binary = '';
|
||||
const CHUNK = 8192;
|
||||
for (let i = 0; i < bytes.length; i += CHUNK) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
```
|
||||
|
||||
Essa correção se aplica a todos os tamanhos de etiqueta, não apenas Niimbot.
|
||||
|
||||
---
|
||||
|
||||
### ✅ NFC Read/Write — NTAG215
|
||||
|
||||
Implementação completa de leitura e gravação NFC para tags NTAG215. O conteúdo gravado é idêntico ao QR Code: `meowspool://filament/{id}` como NDEF URI record.
|
||||
|
||||
@@ -15,14 +15,18 @@ import { formatWeight } from '@shared/utils/filament';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { httpClient } from '@adapters/remote/httpClient';
|
||||
|
||||
type LabelSize = '50x30' | '62x29' | '38x25';
|
||||
type LabelSize = '22x14' | '50x30' | '62x29' | '38x25';
|
||||
|
||||
const LABEL_SIZES: { id: LabelSize; label: string; sub: string }[] = [
|
||||
{ id: '22x14', label: '22 × 14', sub: 'Niimbot D11/D110' },
|
||||
{ id: '50x30', label: '50 × 30', sub: 'Padrão' },
|
||||
{ id: '62x29', label: '62 × 29', sub: 'Brother DK' },
|
||||
{ id: '38x25', label: '38 × 25', sub: 'Dymo 11354' },
|
||||
];
|
||||
|
||||
// Conteúdo mínimo para etiquetas muito pequenas (Niimbot)
|
||||
const MINI_LABEL_CONTENT: Set<ContentOption> = new Set(['color', 'name', 'qrcode']);
|
||||
|
||||
type ContentOption = 'color' | 'name' | 'material_brand' | 'net_weight' | 'print_temp' | 'qrcode';
|
||||
|
||||
const CONTENT_OPTIONS: { id: ContentOption; label: string }[] = [
|
||||
@@ -64,6 +68,13 @@ export default function LabelScreen(): React.ReactElement {
|
||||
|
||||
const pct = calcFilamentPercentage(filament);
|
||||
|
||||
function handleSizeChange(size: LabelSize): void {
|
||||
setSelectedSize(size);
|
||||
if (size === '22x14') {
|
||||
setEnabledContent(new Set(MINI_LABEL_CONTENT));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleContent(opt: ContentOption): void {
|
||||
setEnabledContent((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -78,16 +89,24 @@ export default function LabelScreen(): React.ReactElement {
|
||||
const height_mm = Number(heightStr);
|
||||
const isPdf = selectedFormat === 'pdf';
|
||||
const fields = Array.from(enabledContent).join(',');
|
||||
// DPI alvo: 203 para Niimbot (impressora térmica de baixo custo), 96 para tela
|
||||
const dpi = selectedSize === '22x14' ? 203 : undefined;
|
||||
|
||||
try {
|
||||
if (isPdf) {
|
||||
const response = await httpClient.get<ArrayBuffer>(
|
||||
`/filaments/${id}/label.pdf`,
|
||||
{ params: { width_mm, height_mm, fields }, responseType: 'arraybuffer' },
|
||||
);
|
||||
const base64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(response.data)),
|
||||
{ params: { width_mm, height_mm, fields, dpi }, responseType: 'arraybuffer' },
|
||||
);
|
||||
// Converte ArrayBuffer para Base64 em chunks para evitar stack overflow
|
||||
// em buffers grandes (String.fromCharCode com spread falha acima de ~64KB)
|
||||
const bytes = new Uint8Array(response.data);
|
||||
let binary = '';
|
||||
const CHUNK = 8192;
|
||||
for (let i = 0; i < bytes.length; i += CHUNK) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
const fileUri = `${FileSystem.cacheDirectory}meowspool-label-${id}.pdf`;
|
||||
await FileSystem.writeAsStringAsync(fileUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
@@ -100,7 +119,7 @@ export default function LabelScreen(): React.ReactElement {
|
||||
} else {
|
||||
const response = await httpClient.get<string>(
|
||||
`/filaments/${id}/label.svg`,
|
||||
{ params: { width_mm, height_mm, fields }, responseType: 'text' },
|
||||
{ params: { width_mm, height_mm, fields, dpi }, responseType: 'text' },
|
||||
);
|
||||
const fileUri = `${FileSystem.cacheDirectory}meowspool-label-${id}.svg`;
|
||||
await FileSystem.writeAsStringAsync(fileUri, response.data, { encoding: 'utf8' });
|
||||
@@ -180,7 +199,7 @@ export default function LabelScreen(): React.ReactElement {
|
||||
{LABEL_SIZES.map((s) => (
|
||||
<TouchableOpacity
|
||||
key={s.id}
|
||||
onPress={() => setSelectedSize(s.id)}
|
||||
onPress={() => handleSizeChange(s.id)}
|
||||
style={[styles.sizeChip, selectedSize === s.id && styles.sizeChipActive]}
|
||||
>
|
||||
<Text style={[styles.sizeChipMain, selectedSize === s.id && styles.sizeChipMainActive]}>
|
||||
|
||||
Reference in New Issue
Block a user