Files
MeowSpool/mobile/app/(app)/filaments/[id]/label.tsx
T
Felipe d7fb768d3b feat: add domain models and repositories for user, spool presets, and filaments
- Introduced SpoolPreset and User domain models with necessary DTOs and utility functions.
- Created AuthRepository, FilamentRepository, and SpoolPresetRepository interfaces for authentication and data management.
- Implemented UI components for filament display, including ColorSwatch, FilamentCard, and StockBar.
- Developed layout components such as Header and Screen for consistent app structure.
- Added reusable UI components like Badge, Button, Card, and Input for better user interaction.
- Established global constants and theme settings for consistent styling across the application.
- Implemented utility functions for filament calculations and formatting.
- Created Zustand stores for managing authentication, filament, and preset states.
- Configured TypeScript settings for improved development experience.
2026-03-14 10:36:13 -03:00

308 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import {
View, Text, TouchableOpacity, StyleSheet, Alert,
} from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFilamentStore } from '@store/filamentStore';
import { usePresetStore } from '@store/presetStore';
import { calcFilamentPercentage } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
import { formatWeight } from '@shared/utils/filament';
import { Button } from '@presentation/components/ui/Button';
type LabelSize = '50x30' | '62x29' | '38x25';
const LABEL_SIZES: { id: LabelSize; label: string; sub: string }[] = [
{ id: '50x30', label: '50 × 30', sub: 'Padrão' },
{ id: '62x29', label: '62 × 29', sub: 'Brother DK' },
{ id: '38x25', label: '38 × 25', sub: 'Dymo 11354' },
];
type ContentOption = 'color' | 'name' | 'material_brand' | 'net_weight' | 'print_temp' | 'qrcode';
const CONTENT_OPTIONS: { id: ContentOption; label: string }[] = [
{ id: 'color', label: 'Cor visual' },
{ id: 'name', label: 'Nome' },
{ id: 'material_brand', label: 'Material · Marca' },
{ id: 'net_weight', label: 'Peso líquido' },
{ id: 'print_temp', label: 'Temp. impressão' },
{ id: 'qrcode', label: 'QR Code' },
];
/**
* Tela de Exportar Etiqueta — 1FZ-0
*/
export default function LabelScreen(): React.ReactElement {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const { filaments } = useFilamentStore();
const { presets } = usePresetStore();
const filament = filaments.find((f) => f.id === id);
const preset = filament ? presets.find((p) => p.id === filament.spoolPresetId) : undefined;
const [selectedSize, setSelectedSize] = useState<LabelSize>('50x30');
const [enabledContent, setEnabledContent] = useState<Set<ContentOption>>(
new Set(['color', 'name', 'material_brand', 'net_weight', 'print_temp', 'qrcode']),
);
if (!filament) {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.notFound}>
<Text style={styles.notFoundText}>Filamento não encontrado</Text>
</View>
</SafeAreaView>
);
}
const pct = calcFilamentPercentage(filament);
function toggleContent(opt: ContentOption): void {
setEnabledContent((prev) => {
const next = new Set(prev);
if (next.has(opt)) next.delete(opt); else next.add(opt);
return next;
});
}
function handleExport(): void {
// TODO: Generate SVG and share via expo-sharing
Alert.alert('Exportar SVG', 'Funcionalidade de exportação SVG será implementada na integração com o backend.');
}
const sizeLabel = LABEL_SIZES.find((s) => s.id === selectedSize);
return (
<SafeAreaView style={styles.safe}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Exportar Etiqueta</Text>
<View style={{ width: 24 }} />
</View>
<View style={styles.content}>
{/* Label Preview */}
<Text style={styles.sectionLabel}>PREVIEW DA ETIQUETA</Text>
<View style={styles.previewCard}>
<View style={[styles.previewColorBar, { backgroundColor: filament.colorHex }]} />
<View style={styles.previewBody}>
<View style={styles.previewTop}>
<View style={styles.previewInfo}>
<View style={styles.previewNameRow}>
<View style={[styles.previewDot, { backgroundColor: filament.colorHex }]} />
<Text style={styles.previewName}>
{filament.brand}{filament.model ? ` ${filament.model}` : ''}
</Text>
</View>
<Text style={styles.previewMeta}>
{filament.brand} · {filament.material} · {filament.colorHex}
</Text>
<View style={styles.previewParamsRow}>
<View>
<Text style={styles.previewParamLabel}>DISPONÍVEL</Text>
<Text style={styles.previewParamValue}>{formatWeight(filament.netWeightG)}</Text>
</View>
{filament.tempHotendC && (
<View>
<Text style={styles.previewParamLabel}>HOTEND</Text>
<Text style={styles.previewParamValue}>{filament.tempHotendC}°C</Text>
</View>
)}
{filament.tempBedC && (
<View>
<Text style={styles.previewParamLabel}>MESA</Text>
<Text style={styles.previewParamValue}>{filament.tempBedC}°C</Text>
</View>
)}
</View>
</View>
{/* QR placeholder */}
<Ionicons name="qr-code" size={48} color={colors.textSecondary} />
</View>
</View>
</View>
<Text style={styles.previewSize}>
<Ionicons name="resize-outline" size={12} color={colors.textSecondary} />{' '}
{sizeLabel?.label} mm ({sizeLabel?.sub})
</Text>
{/* Tamanho */}
<Text style={[styles.sectionLabel, { marginTop: spacing[2] }]}>TAMANHO</Text>
<View style={styles.sizeChips}>
{LABEL_SIZES.map((s) => (
<TouchableOpacity
key={s.id}
onPress={() => setSelectedSize(s.id)}
style={[styles.sizeChip, selectedSize === s.id && styles.sizeChipActive]}
>
<Text style={[styles.sizeChipMain, selectedSize === s.id && styles.sizeChipMainActive]}>
{s.label}
</Text>
<Text style={[styles.sizeChipSub, selectedSize === s.id && styles.sizeChipSubActive]}>
{s.sub}
</Text>
</TouchableOpacity>
))}
</View>
{/* Conteúdo */}
<Text style={styles.sectionLabel}>CONTEÚDO DA ETIQUETA</Text>
<View style={styles.contentOptions}>
{CONTENT_OPTIONS.map((opt) => {
const active = enabledContent.has(opt.id);
return (
<TouchableOpacity
key={opt.id}
onPress={() => toggleContent(opt.id)}
style={[styles.contentChip, active && styles.contentChipActive]}
>
{active && <Ionicons name="checkmark" size={12} color={colors.accent} />}
<Text style={[styles.contentChipText, active && styles.contentChipTextActive]}>
{opt.label}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
{/* CTA */}
<View style={styles.footer}>
<Button
label="Exportar SVG"
leftIcon={<Ionicons name="download-outline" size={18} color={colors.bgBase} />}
onPress={handleExport}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.bgBase },
notFound: { flex: 1, alignItems: 'center', justifyContent: 'center' },
notFoundText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textSecondary },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing[5],
paddingVertical: spacing[4],
},
headerTitle: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.textPrimary,
},
content: { flex: 1, paddingHorizontal: spacing[5], gap: spacing[4] },
sectionLabel: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.textSecondary,
letterSpacing: 0.8,
textTransform: 'uppercase',
},
previewCard: {
backgroundColor: '#F5EFE0',
borderRadius: radius.lg,
flexDirection: 'row',
overflow: 'hidden',
minHeight: 100,
},
previewColorBar: { width: 10 },
previewBody: { flex: 1, padding: spacing[3] },
previewTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' },
previewInfo: { flex: 1, gap: 4 },
previewNameRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
previewDot: { width: 12, height: 12, borderRadius: radius.full },
previewName: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.black,
},
previewMeta: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xs,
color: '#666',
},
previewParamsRow: { flexDirection: 'row', gap: spacing[4], marginTop: 4 },
previewParamLabel: {
fontFamily: typography.fontFamily.ui,
fontSize: 9,
color: '#888',
textTransform: 'uppercase',
},
previewParamValue: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold,
color: colors.black,
},
previewSize: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
textAlign: 'right',
marginTop: -spacing[2],
},
sizeChips: { flexDirection: 'row', gap: spacing[2] },
sizeChip: {
flex: 1,
alignItems: 'center',
paddingVertical: spacing[3],
borderRadius: radius.lg,
backgroundColor: colors.bgSurface,
borderWidth: 1,
borderColor: colors.border,
},
sizeChipActive: { borderColor: colors.accent, backgroundColor: colors.accentMuted },
sizeChipMain: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.textSecondary,
},
sizeChipMainActive: { color: colors.accent },
sizeChipSub: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
marginTop: 2,
},
sizeChipSubActive: { color: colors.accent },
contentOptions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing[2] },
contentChip: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: spacing[3],
paddingVertical: spacing[2],
borderRadius: radius.full,
backgroundColor: colors.bgHover,
borderWidth: 1,
borderColor: colors.border,
},
contentChipActive: { borderColor: colors.accent },
contentChipText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xs,
color: colors.textSecondary,
},
contentChipTextActive: { color: colors.accent },
footer: {
padding: spacing[5],
borderTopWidth: 1,
borderTopColor: colors.border,
backgroundColor: colors.bgBase,
},
});