feat(app): FightRateApp tab fade + brand LiveDot + tab live indicator
This commit is contained in:
1 parent
55fb37a34a
commit
1cb658f899
1 file changed
+92
-94
+92
-94
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
@@ -16,12 +16,19 @@ import {
|
||||
type LucideIcon,
|
||||
} from 'lucide-react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { useHeartRateMonitor } from '../ble/useHeartRateMonitor';
|
||||
import { LiveDot } from '../components/LiveDot';
|
||||
import { CaptureScreen } from '../screens/CaptureScreen';
|
||||
import { HistoryScreen } from '../screens/HistoryScreen';
|
||||
import { InsightsScreen } from '../screens/InsightsScreen';
|
||||
import { SensorScreen } from '../screens/SensorScreen';
|
||||
import { colors, radius, spacing, typography } from '../theme';
|
||||
import { colors, motion, radius, spacing, typography } from '../theme';
|
||||
|
||||
type TabKey = 'capture' | 'history' | 'sensor' | 'insights';
|
||||
|
||||
@@ -33,7 +40,7 @@ type Tab = {
|
||||
|
||||
const tabs: readonly Tab[] = [
|
||||
{ key: 'capture', label: 'Captura', icon: Radio },
|
||||
{ key: 'history', label: 'Histórico', icon: History },
|
||||
{ key: 'history', label: 'Hist\órico', icon: History },
|
||||
{ key: 'sensor', label: 'Sensor', icon: Bluetooth },
|
||||
{ key: 'insights', label: 'Insights', icon: ChartNoAxesCombined },
|
||||
];
|
||||
@@ -42,13 +49,23 @@ export function FightRateApp() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const monitor = useHeartRateMonitor();
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('capture');
|
||||
const opacity = useSharedValue(1);
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = 0;
|
||||
opacity.value = withTiming(1, {
|
||||
duration: motion.duration.fast,
|
||||
easing: Easing.out(Easing.quad),
|
||||
});
|
||||
}, [activeTab, opacity]);
|
||||
|
||||
const fadeStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
||||
|
||||
const onCaptureAction = useCallback(async () => {
|
||||
if (monitor.isCapturing) {
|
||||
await monitor.stopCapture();
|
||||
return;
|
||||
}
|
||||
|
||||
await monitor.startCapture();
|
||||
}, [monitor]);
|
||||
|
||||
@@ -57,31 +74,22 @@ export function FightRateApp() {
|
||||
await monitor.stopMonitoring();
|
||||
return;
|
||||
}
|
||||
|
||||
await monitor.startMonitoring();
|
||||
}, [monitor]);
|
||||
|
||||
const activeTitle = useMemo(() => {
|
||||
if (activeTab === 'capture') {
|
||||
if (monitor.isCapturing) {
|
||||
return 'Janela de treino ativa';
|
||||
}
|
||||
if (monitor.isMonitoring) {
|
||||
return 'Monitor contínuo';
|
||||
}
|
||||
if (monitor.isCapturing) return 'Janela de treino ativa';
|
||||
if (monitor.isMonitoring) return 'Monitor cont\ínuo';
|
||||
return 'Ativar sensor';
|
||||
}
|
||||
|
||||
if (activeTab === 'history') {
|
||||
return 'Histórico real';
|
||||
}
|
||||
|
||||
if (activeTab === 'sensor') {
|
||||
return 'Sensor BLE';
|
||||
}
|
||||
|
||||
return 'Análise real';
|
||||
if (activeTab === 'history') return 'Hist\órico real';
|
||||
if (activeTab === 'sensor') return 'Sensor BLE';
|
||||
return 'An\álise real';
|
||||
}, [activeTab, monitor.isCapturing, monitor.isMonitoring]);
|
||||
|
||||
const isAnyLive = monitor.isMonitoring || monitor.isCapturing;
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<StatusBar barStyle="light-content" backgroundColor={colors.canvas} />
|
||||
@@ -101,43 +109,54 @@ export function FightRateApp() {
|
||||
style={styles.logo}
|
||||
/>
|
||||
<View style={styles.titleColumn}>
|
||||
<Text style={styles.kicker}>FightRate</Text>
|
||||
<View style={styles.kickerRow}>
|
||||
{isAnyLive ? <LiveDot live size={6} /> : null}
|
||||
<Text style={styles.kicker}>FightRate</Text>
|
||||
</View>
|
||||
<Text style={styles.title}>{activeTitle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{activeTab === 'capture' ? (
|
||||
<CaptureScreen
|
||||
monitor={monitor}
|
||||
onCaptureAction={onCaptureAction}
|
||||
onMonitorAction={onMonitorAction}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'history' ? (
|
||||
<HistoryScreen
|
||||
sessions={monitor.sessions}
|
||||
getSessionSamples={monitor.getSessionSamples}
|
||||
getContinuousSamples={monitor.getContinuousSamples}
|
||||
liveSamples={monitor.samples}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'sensor' ? (
|
||||
<SensorScreen monitor={monitor} onMonitorAction={onMonitorAction} />
|
||||
) : null}
|
||||
{activeTab === 'insights' ? (
|
||||
<InsightsScreen
|
||||
liveSamples={monitor.samples}
|
||||
sessions={monitor.sessions}
|
||||
getContinuousSamples={monitor.getContinuousSamples}
|
||||
/>
|
||||
) : null}
|
||||
<Animated.View style={fadeStyle} key={`tab-${activeTab}`}>
|
||||
{activeTab === 'capture' ? (
|
||||
<CaptureScreen
|
||||
monitor={monitor}
|
||||
onCaptureAction={onCaptureAction}
|
||||
onMonitorAction={onMonitorAction}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'history' ? (
|
||||
<HistoryScreen
|
||||
sessions={monitor.sessions}
|
||||
getSessionSamples={monitor.getSessionSamples}
|
||||
getContinuousSamples={monitor.getContinuousSamples}
|
||||
liveSamples={monitor.samples}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'sensor' ? (
|
||||
<SensorScreen
|
||||
monitor={monitor}
|
||||
onMonitorAction={onMonitorAction}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'insights' ? (
|
||||
<InsightsScreen
|
||||
liveSamples={monitor.samples}
|
||||
sessions={monitor.sessions}
|
||||
getContinuousSamples={monitor.getContinuousSamples}
|
||||
/>
|
||||
) : null}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.navWrap, { bottom: insets.bottom + spacing.sm }]}>
|
||||
<View style={styles.navRail}>
|
||||
{tabs.map(tab => {
|
||||
{tabs.map((tab) => {
|
||||
const selected = tab.key === activeTab;
|
||||
const Icon = tab.icon;
|
||||
const isTabLive =
|
||||
(tab.key === 'capture' && monitor.isCapturing) ||
|
||||
(tab.key === 'sensor' && monitor.isMonitoring);
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="tab"
|
||||
@@ -146,11 +165,18 @@ export function FightRateApp() {
|
||||
onPress={() => setActiveTab(tab.key)}
|
||||
style={[styles.navItem, selected ? styles.navItemActive : null]}
|
||||
>
|
||||
<Icon
|
||||
color={selected ? colors.text : colors.textMuted}
|
||||
size={18}
|
||||
strokeWidth={2.4}
|
||||
/>
|
||||
<View style={styles.navIconRow}>
|
||||
<Icon
|
||||
color={selected ? colors.text : colors.textMuted}
|
||||
size={18}
|
||||
strokeWidth={2.4}
|
||||
/>
|
||||
{isTabLive ? (
|
||||
<View style={styles.tabLiveDot}>
|
||||
<LiveDot live size={4} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.navLabel,
|
||||
@@ -169,31 +195,13 @@ export function FightRateApp() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.canvas,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: spacing.md,
|
||||
gap: spacing.lg,
|
||||
},
|
||||
brandRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
logo: {
|
||||
width: 54,
|
||||
height: 54,
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
titleColumn: {
|
||||
flex: 1,
|
||||
},
|
||||
kicker: {
|
||||
...typography.label,
|
||||
color: colors.action,
|
||||
},
|
||||
root: { flex: 1, backgroundColor: colors.canvas },
|
||||
content: { paddingHorizontal: spacing.md, gap: spacing.lg },
|
||||
brandRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md },
|
||||
logo: { width: 54, height: 54, borderRadius: radius.sm },
|
||||
titleColumn: { flex: 1 },
|
||||
kickerRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
kicker: { ...typography.label, color: colors.action },
|
||||
title: {
|
||||
marginTop: spacing.xs,
|
||||
color: colors.text,
|
||||
@@ -202,11 +210,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '900',
|
||||
letterSpacing: -2.1,
|
||||
},
|
||||
navWrap: {
|
||||
position: 'absolute',
|
||||
left: spacing.md,
|
||||
right: spacing.md,
|
||||
},
|
||||
navWrap: { position: 'absolute', left: spacing.md, right: spacing.md },
|
||||
navRail: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.line,
|
||||
@@ -228,15 +232,9 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
gap: spacing.xxs,
|
||||
},
|
||||
navItemActive: {
|
||||
backgroundColor: colors.action,
|
||||
},
|
||||
navLabel: {
|
||||
...typography.label,
|
||||
color: colors.textMuted,
|
||||
fontSize: 9,
|
||||
},
|
||||
navLabelActive: {
|
||||
color: colors.text,
|
||||
},
|
||||
navItemActive: { backgroundColor: colors.action },
|
||||
navIconRow: { position: 'relative' },
|
||||
tabLiveDot: { position: 'absolute', top: -2, right: -6 },
|
||||
navLabel: { ...typography.label, color: colors.textMuted, fontSize: 9 },
|
||||
navLabelActive: { color: colors.text },
|
||||
});
|
||||
Reference in new issue
Block a user