feat: enhance filament management and UI improvements

- Update SpoolPresetRepository to include user_id in the update query.
- Expand .gitignore to include various environment and temporary files.
- Revise agent documentation for better clarity and formatting.
- Implement pull-to-refresh functionality in the inventory list.
- Integrate API calls for deleting and updating filaments, ensuring state synchronization.
- Add custom color picker for filament color selection with hex validation.
- Update AndroidManifest and Gradle files for improved configuration and permissions.
- Refactor MainActivity and MainApplication for better splash screen handling.
- Update styles and colors for a cohesive UI experience.
- Replace splash screen logos and icons with new assets.
This commit is contained in:
2026-03-14 14:14:06 -03:00
parent 416e13893c
commit 9c0b3d584c
33 changed files with 519 additions and 164 deletions
+21 -2
View File
@@ -1,11 +1,13 @@
import React, { useState } from 'react';
import {
View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput,
View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, RefreshControl,
} from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFilamentStore } from '@store/filamentStore';
import { listFilamentsUseCase } from '@infrastructure/container';
import { useAuthStore } from '@store/authStore';
import { FilamentCard } from '@presentation/components/filament/FilamentCard';
import type { Filament } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
@@ -19,9 +21,11 @@ const TABS = ['Todos', ...MATERIALS.slice(0, 4)] as const;
*/
export default function InventoryScreen(): React.ReactElement {
const router = useRouter();
const { filaments } = useFilamentStore();
const { filaments, setFilaments } = useFilamentStore();
const { user } = useAuthStore();
const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState<string>('Todos');
const [isRefreshing, setIsRefreshing] = useState(false);
const filtered = filaments.filter((f) => {
const matchMaterial = activeTab === 'Todos' || f.material === activeTab;
@@ -33,6 +37,18 @@ export default function InventoryScreen(): React.ReactElement {
return matchMaterial && matchSearch;
});
async function onRefresh(): Promise<void> {
setIsRefreshing(true);
try {
const updated = await listFilamentsUseCase.execute(user?.id || '');
setFilaments(updated);
} catch (err) {
console.error('refresh filaments error', err);
} finally {
setIsRefreshing(false);
}
}
function renderItem({ item }: { item: Filament }): React.ReactElement {
return <FilamentCard filament={item} />;
}
@@ -96,6 +112,9 @@ export default function InventoryScreen(): React.ReactElement {
contentContainerStyle={styles.list}
ItemSeparatorComponent={() => <View style={styles.separator} />}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} tintColor={colors.accent} />
}
ListEmptyComponent={
<View style={styles.empty}>
<Ionicons name="layers-outline" size={40} color={colors.textSecondary} />
+12 -3
View File
@@ -7,6 +7,8 @@ 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 { useAuthStore } from '@store/authStore';
import { deleteFilamentUseCase } from '@infrastructure/container';
import { calcFilamentPercentage } from '@domain/Filament';
import { colors, typography, spacing, radius, getStockColor, getStockBgColor } from '@shared/theme';
import { formatWeight } from '@shared/utils/filament';
@@ -52,9 +54,16 @@ export default function FilamentDetailScreen(): React.ReactElement {
{
text: 'Excluir',
style: 'destructive',
onPress: () => {
removeFilament(filament!.id);
router.back();
onPress: async () => {
try {
const { user } = useAuthStore.getState();
await deleteFilamentUseCase.execute(filament!.id, user?.id || '');
removeFilament(filament!.id);
router.back();
} catch (err) {
console.error('delete filament error', err);
Alert.alert('Erro', 'Não foi possível deletar o filamento.');
}
},
},
],
+56 -10
View File
@@ -10,13 +10,15 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { usePresetStore } from '@store/presetStore';
import { useFilamentStore } from '@store/filamentStore';
import { useAuthStore } from '@store/authStore';
import { deleteFilamentUseCase, updateFilamentUseCase } from '@infrastructure/container';
import { Input } from '@presentation/components/ui/Input';
import { Button } from '@presentation/components/ui/Button';
import { Card } from '@presentation/components/ui/Card';
import { calcNetWeight } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
import { MATERIALS, type Material } from '@shared/constants';
import { formatWeight } from '@shared/utils/filament';
import { formatWeight, isValidHex, normalizeHex } from '@shared/utils/filament';
const schema = z.object({
brand: z.string().min(1, 'Marca obrigatória'),
@@ -48,6 +50,7 @@ export default function EditFilamentScreen(): React.ReactElement {
const [selectedMaterial, setSelectedMaterial] = useState<Material>((filament?.material as Material) ?? 'PLA');
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(filament?.spoolPresetId ?? systemPresets[0]?.id ?? null);
const [isLoading, setIsLoading] = useState(false);
const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
const { control, handleSubmit, watch, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
@@ -82,23 +85,66 @@ export default function EditFilamentScreen(): React.ReactElement {
);
}
function handleDelete(): void {
const handleOpenColorPicker = (): void => {
if (isOpeningColorPicker) return;
setIsOpeningColorPicker(true);
Alert.prompt(
'Cor Customizada',
'Digite um código de cor hexadecimal (ex: #FF5733)',
[
{ text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
{
text: 'Confirmar',
onPress: (value) => {
setIsOpeningColorPicker(false);
if (!value) return;
const normalized = normalizeHex(value.trim());
if (isValidHex(normalized)) {
setSelectedColor(normalized);
setHexInput(normalized);
} else {
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
}
},
},
],
'plain-text',
selectedColor,
);
};
const handleDelete = async (): Promise<void> => {
Alert.alert(
'Excluir filamento',
`Deseja excluir "${filament!.brand}${filament!.model ? ` ${filament!.model}` : ''}"? Esta ação não pode ser desfeita.`,
'Confirmar Deleção',
'Tem certeza que deseja deletar este filamento?',
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Excluir',
text: 'Deletar',
style: 'destructive',
onPress: () => {
removeFilament(filament!.id);
router.replace('/(app)/(tabs)/inventory');
onPress: async () => {
const user = useAuthStore.getState().user;
if (!user) {
Alert.alert('Erro', 'Usuário não autenticado.');
return;
}
setIsLoading(true);
try {
await deleteFilamentUseCase.execute(filament!.id, user.id);
removeFilament(filament!.id);
Alert.alert('Sucesso', 'Filamento deletado.');
router.back();
} catch (err) {
console.error('delete filament error', err);
Alert.alert('Erro', 'Falha ao deletar: ' + (err as Error).message);
} finally {
setIsLoading(false);
}
},
},
],
);
}
};
async function onSubmit(data: FormData): Promise<void> {
if (!selectedPresetId) {
@@ -163,7 +209,7 @@ export default function EditFilamentScreen(): React.ReactElement {
style={[styles.colorDot, { backgroundColor: c }, selectedColor === c && styles.colorDotActive]}
/>
))}
<TouchableOpacity style={styles.colorAddBtn}>
<TouchableOpacity style={styles.colorAddBtn} onPress={handleOpenColorPicker}>
<Ionicons name="add" size={18} color={colors.textSecondary} />
</TouchableOpacity>
</View>
+32 -2
View File
@@ -19,6 +19,7 @@ import { colors, typography, spacing, radius } from '@shared/theme';
import { MATERIALS, type Material } from '@shared/constants';
import { formatWeight } from '@shared/utils/filament';
import { createFilamentUseCase } from '@infrastructure/container';
import { isValidHex, normalizeHex } from '@shared/utils/filament';
const schema = z.object({
brand: z.string().min(1, 'Marca obrigatória'),
@@ -48,6 +49,7 @@ export default function NewFilamentScreen(): React.ReactElement {
const [selectedMaterial, setSelectedMaterial] = useState<Material>('PLA');
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(systemPresets[0]?.id ?? null);
const [isLoading, setIsLoading] = useState(false);
const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
// Quando os presets carregam (via layout), seleciona o primeiro sistema automaticamente
useEffect(() => {
@@ -67,7 +69,35 @@ export default function NewFilamentScreen(): React.ReactElement {
? calcNetWeight(Number(totalWeight), selectedPreset.spoolWeightG)
: 0;
const pct = Math.min(100, Math.round((netWeight / 1000) * 100));
function handleOpenColorPicker(): void {
if (isOpeningColorPicker) return;
setIsOpeningColorPicker(true);
Alert.prompt(
'Cor Customizada',
'Digite um código de cor hexadecimal (ex: #FF5733)',
[
{ text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
{
text: 'Confirmar',
onPress: (value) => {
setIsOpeningColorPicker(false);
if (!value) return;
const normalized = normalizeHex(value.trim());
if (isValidHex(normalized)) {
setSelectedColor(normalized);
setHexInput(normalized);
} else {
Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
}
},
},
],
'plain-text',
selectedColor,
);
}
async function onSubmit(data: FormData): Promise<void> {
if (!selectedPresetId) {
Alert.alert('Atenção', 'Selecione um preset de carretel.');
@@ -102,7 +132,7 @@ export default function NewFilamentScreen(): React.ReactElement {
}
return (
<SafeAreaView style={styles.safe}>
<SafeAreaView style={styles.safe}> onPress={handleOpenColorPicker}
<ScrollView showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
{/* Header */}
<View style={styles.header}>
@@ -130,7 +160,7 @@ export default function NewFilamentScreen(): React.ReactElement {
style={[styles.colorDot, { backgroundColor: c }, selectedColor === c && styles.colorDotActive]}
/>
))}
<TouchableOpacity style={styles.colorAddBtn}>
<TouchableOpacity style={styles.colorAddBtn} onPress={handleOpenColorPicker}>
<Ionicons name="add" size={18} color={colors.textSecondary} />
</TouchableOpacity>
</View>