Compare commits
2 Commits
e725819c01
...
f3cf10e5e6
| Author | SHA1 | Date | |
|---|---|---|---|
| f3cf10e5e6 | |||
| 862c4e42a4 |
234
THEME_GUIDE.md
Normal file
234
THEME_GUIDE.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Theme System Documentation
|
||||
|
||||
## Tổng quan
|
||||
|
||||
Hệ thống theme đã được cấu hình để hỗ trợ Light Mode, Dark Mode và System Mode (tự động theo hệ thống). Theme được lưu trữ trong AsyncStorage và sẽ được khôi phục khi khởi động lại ứng dụng.
|
||||
|
||||
## Cấu trúc Theme
|
||||
|
||||
### 1. Colors Configuration (`constants/theme.ts`)
|
||||
|
||||
```typescript
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: "#11181C",
|
||||
textSecondary: "#687076",
|
||||
background: "#fff",
|
||||
backgroundSecondary: "#f5f5f5",
|
||||
surface: "#ffffff",
|
||||
surfaceSecondary: "#f8f9fa",
|
||||
primary: "#007AFF",
|
||||
secondary: "#5AC8FA",
|
||||
success: "#34C759",
|
||||
warning: "#FF9500",
|
||||
error: "#FF3B30",
|
||||
// ... more colors
|
||||
},
|
||||
dark: {
|
||||
text: "#ECEDEE",
|
||||
textSecondary: "#8E8E93",
|
||||
background: "#000000",
|
||||
backgroundSecondary: "#1C1C1E",
|
||||
surface: "#1C1C1E",
|
||||
surfaceSecondary: "#2C2C2E",
|
||||
primary: "#0A84FF",
|
||||
secondary: "#64D2FF",
|
||||
success: "#30D158",
|
||||
warning: "#FF9F0A",
|
||||
error: "#FF453A",
|
||||
// ... more colors
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Theme Context (`hooks/use-theme-context.tsx`)
|
||||
|
||||
Cung cấp theme state và functions cho toàn bộ app:
|
||||
|
||||
```typescript
|
||||
interface ThemeContextType {
|
||||
themeMode: ThemeMode; // 'light' | 'dark' | 'system'
|
||||
colorScheme: ColorScheme; // 'light' | 'dark'
|
||||
colors: typeof Colors.light;
|
||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||
getColor: (colorName: ColorName) => string;
|
||||
}
|
||||
```
|
||||
|
||||
## Cách sử dụng Theme
|
||||
|
||||
### 1. Sử dụng Themed Components
|
||||
|
||||
```tsx
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<ThemedView>
|
||||
<ThemedText type="title">Title Text</ThemedText>
|
||||
<ThemedText type="default">Regular Text</ThemedText>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Sử dụng Theme Hook
|
||||
|
||||
```tsx
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
function MyComponent() {
|
||||
const { colors, colorScheme, setThemeMode } = useThemeContext();
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: colors.background }}>
|
||||
<Text style={{ color: colors.text }}>Current theme: {colorScheme}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sử dụng App Theme Hook (Recommended)
|
||||
|
||||
```tsx
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
|
||||
function MyComponent() {
|
||||
const { colors, styles, utils } = useAppTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>Button</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.surface,
|
||||
{
|
||||
backgroundColor: utils.getOpacityColor("primary", 0.1),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={{ color: colors.text }}>
|
||||
Theme is {utils.isDark ? "Dark" : "Light"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Sử dụng useThemeColor Hook
|
||||
|
||||
```tsx
|
||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||
|
||||
function MyComponent() {
|
||||
// Override colors for specific themes
|
||||
const backgroundColor = useThemeColor(
|
||||
{ light: "#ffffff", dark: "#1C1C1E" },
|
||||
"surface"
|
||||
);
|
||||
|
||||
const textColor = useThemeColor({}, "text");
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor }}>
|
||||
<Text style={{ color: textColor }}>Text</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Theme Toggle Component
|
||||
|
||||
Sử dụng `ThemeToggle` component để cho phép user chọn theme:
|
||||
|
||||
```tsx
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
function SettingsScreen() {
|
||||
return (
|
||||
<View>
|
||||
<ThemeToggle />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Available Styles từ useAppTheme
|
||||
|
||||
```typescript
|
||||
const { styles } = useAppTheme();
|
||||
|
||||
// Container styles
|
||||
styles.container; // Flex 1 container với background
|
||||
styles.surface; // Card surface với padding
|
||||
styles.card; // Card với shadow và border radius
|
||||
|
||||
// Button styles
|
||||
styles.primaryButton; // Primary button style
|
||||
styles.secondaryButton; // Secondary button với border
|
||||
styles.primaryButtonText; // White text cho primary button
|
||||
styles.secondaryButtonText; // Theme text cho secondary button
|
||||
|
||||
// Input styles
|
||||
styles.textInput; // Text input với border và padding
|
||||
|
||||
// Status styles
|
||||
styles.successContainer; // Success status container
|
||||
styles.warningContainer; // Warning status container
|
||||
styles.errorContainer; // Error status container
|
||||
|
||||
// Utility
|
||||
styles.separator; // Line separator
|
||||
```
|
||||
|
||||
## Theme Utilities
|
||||
|
||||
```typescript
|
||||
const { utils } = useAppTheme();
|
||||
|
||||
utils.isDark; // boolean - kiểm tra dark mode
|
||||
utils.isLight; // boolean - kiểm tra light mode
|
||||
utils.toggleTheme(); // function - toggle giữa light/dark
|
||||
utils.getOpacityColor(colorName, opacity); // Tạo màu với opacity
|
||||
```
|
||||
|
||||
## Lưu trữ Theme Preference
|
||||
|
||||
Theme preference được tự động lưu trong AsyncStorage với key `'theme_mode'`. Khi app khởi động, theme sẽ được khôi phục từ storage.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Sử dụng `useAppTheme`** thay vì access colors trực tiếp
|
||||
2. **Sử dụng pre-defined styles** từ `useAppTheme().styles`
|
||||
3. **Kiểm tra theme** bằng `utils.isDark` thay vì check colorScheme
|
||||
4. **Sử dụng opacity colors** cho backgrounds: `utils.getOpacityColor('primary', 0.1)`
|
||||
5. **Tận dụng ThemedText và ThemedView** cho các component đơn giản
|
||||
|
||||
## Migration từ theme cũ
|
||||
|
||||
Nếu bạn đang sử dụng theme cũ:
|
||||
|
||||
```tsx
|
||||
// Cũ
|
||||
const colorScheme = useColorScheme();
|
||||
const backgroundColor = colorScheme === "dark" ? "#000" : "#fff";
|
||||
|
||||
// Mới
|
||||
const { colors } = useAppTheme();
|
||||
const backgroundColor = colors.background;
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Theme không được lưu**: Kiểm tra AsyncStorage permissions
|
||||
2. **Flash khi khởi động**: ThemeProvider sẽ chờ load theme trước khi render
|
||||
3. **Colors không đúng**: Đảm bảo component được wrap trong ThemeProvider
|
||||
|
||||
## Examples
|
||||
|
||||
Xem `components/theme-example.tsx` để biết các cách sử dụng theme khác nhau.
|
||||
29
app.json
29
app.json
@@ -9,7 +9,16 @@
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"infoPlist": {
|
||||
"CFBundleLocalizations": [
|
||||
"en",
|
||||
"vi",
|
||||
"en",
|
||||
"vi"
|
||||
]
|
||||
},
|
||||
"bundleIdentifier": "com.minhnn86.sgwapp"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@@ -19,7 +28,12 @@
|
||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"permissions": [
|
||||
"android.permission.CAMERA",
|
||||
"android.permission.RECORD_AUDIO"
|
||||
],
|
||||
"package": "com.minhnn86.sgwapp"
|
||||
},
|
||||
"web": {
|
||||
"output": "static",
|
||||
@@ -28,6 +42,7 @@
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-system-ui",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
@@ -50,8 +65,14 @@
|
||||
"expo-localization",
|
||||
{
|
||||
"supportedLocales": {
|
||||
"ios": ["en", "vi"],
|
||||
"android": ["en", "vi"]
|
||||
"ios": [
|
||||
"en",
|
||||
"vi"
|
||||
],
|
||||
"android": [
|
||||
"en",
|
||||
"vi"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/constants";
|
||||
import { useColorScheme } from "@/hooks/use-color-scheme.web";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import {
|
||||
getAlarmEventBus,
|
||||
getBanzonesEventBus,
|
||||
@@ -53,7 +54,7 @@ export default function HomeScreen() {
|
||||
PolygonWithLabelProps[]
|
||||
>([]);
|
||||
const platform = usePlatform();
|
||||
const theme = useColorScheme();
|
||||
const theme = useThemeContext().colorScheme;
|
||||
const scale = useRef(new Animated.Value(0)).current;
|
||||
const opacity = useRef(new Animated.Value(1)).current;
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { StyleSheet, View, ScrollView } from "react-native";
|
||||
|
||||
import EnIcon from "@/assets/icons/en_icon.png";
|
||||
import VnIcon from "@/assets/icons/vi_icon.png";
|
||||
import RotateSwitch from "@/components/rotate-switch";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { DOMAIN, TOKEN } from "@/constants";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { removeStorageItem } from "@/utils/storage";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
type Todo = {
|
||||
userId: number;
|
||||
id: number;
|
||||
@@ -21,7 +24,9 @@ export default function SettingScreen() {
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Todo | null>(null);
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
const [isEnabled, setIsEnabled] = useState(locale === "vi");
|
||||
|
||||
// Sync isEnabled state khi locale thay đổi
|
||||
useEffect(() => {
|
||||
setIsEnabled(locale === "vi");
|
||||
@@ -33,18 +38,31 @@ export default function SettingScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">{t("navigation.setting")}</ThemedText>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
{t("navigation.setting")}
|
||||
</ThemedText>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
{/* Theme Toggle Section */}
|
||||
<ThemeToggle style={styles.themeSection} />
|
||||
|
||||
{/* Language Section */}
|
||||
<View
|
||||
style={[
|
||||
styles.settingItem,
|
||||
{
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ThemedText type="default">{t("common.language")}</ThemedText>
|
||||
{/* <Switch
|
||||
trackColor={{ false: "#767577", true: "#81b0ff" }}
|
||||
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
|
||||
ios_backgroundColor="#3e3e3e"
|
||||
onValueChange={toggleSwitch}
|
||||
value={isEnabled}
|
||||
/> */}
|
||||
<RotateSwitch
|
||||
initialValue={isEnabled}
|
||||
onChange={toggleSwitch}
|
||||
@@ -54,51 +72,82 @@ export default function SettingScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Logout Button */}
|
||||
<ThemedView
|
||||
style={styles.button}
|
||||
style={[styles.button, { backgroundColor: colors.primary }]}
|
||||
onTouchEnd={async () => {
|
||||
await removeStorageItem(TOKEN);
|
||||
await removeStorageItem(DOMAIN);
|
||||
router.navigate("/auth/login");
|
||||
}}
|
||||
>
|
||||
<ThemedText type="defaultSemiBold">{t("auth.logout")}</ThemedText>
|
||||
<ThemedText type="defaultSemiBold" style={styles.buttonText}>
|
||||
{t("auth.logout")}
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
{data && (
|
||||
<ThemedView style={{ marginTop: 20 }}>
|
||||
<ThemedView
|
||||
style={[styles.debugSection, { backgroundColor: colors.surface }]}
|
||||
>
|
||||
<ThemedText type="default">{data.title}</ThemedText>
|
||||
<ThemedText type="default">{data.completed}</ThemedText>
|
||||
<ThemedText type="default">{data.id}</ThemedText>
|
||||
</ThemedView>
|
||||
)}
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: 20,
|
||||
gap: 16,
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
marginBottom: 20,
|
||||
},
|
||||
themeSection: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "rgba(0, 122, 255, 0.1)",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
button: {
|
||||
marginTop: 20,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: "#007AFF",
|
||||
borderRadius: 8,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
debugSection: {
|
||||
marginTop: 20,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,19 +5,18 @@ import CrewListTable from "@/components/tripInfo/CrewListTable";
|
||||
import FishingToolsTable from "@/components/tripInfo/FishingToolsList";
|
||||
import NetListTable from "@/components/tripInfo/NetListTable";
|
||||
import TripCostTable from "@/components/tripInfo/TripCostTable";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export default function TripInfoScreen() {
|
||||
// const { trip, getTrip } = useTrip();
|
||||
// useEffect(() => {
|
||||
// getTrip();
|
||||
// }, []);
|
||||
|
||||
const { colors } = useThemeContext();
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea} edges={["top", "left", "right"]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.titleText}>Thông Tin Chuyến Đi</Text>
|
||||
<Text style={[styles.titleText, { color: colors.text }]}>
|
||||
Thông Tin Chuyến Đi
|
||||
</Text>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<ButtonCreateNewHaulOrTrip />
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
DarkTheme,
|
||||
DefaultTheme,
|
||||
ThemeProvider,
|
||||
ThemeProvider as NavigationThemeProvider,
|
||||
} from "@react-navigation/native";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
@@ -13,21 +13,21 @@ import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider/glues
|
||||
import { toastConfig } from "@/config";
|
||||
import { setRouterInstance } from "@/config/auth";
|
||||
import "@/global.css";
|
||||
import { useColorScheme } from "@/hooks/use-color-scheme";
|
||||
import { ThemeProvider, useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { I18nProvider } from "@/hooks/use-i18n";
|
||||
import Toast from "react-native-toast-message";
|
||||
import "../global.css";
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
function AppContent() {
|
||||
const { colorScheme } = useThemeContext();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setRouterInstance(router);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<GluestackUIProvider>
|
||||
<ThemeProvider
|
||||
<GluestackUIProvider mode={colorScheme}>
|
||||
<NavigationThemeProvider
|
||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
||||
>
|
||||
<Stack
|
||||
@@ -57,8 +57,17 @@ export default function RootLayout() {
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
|
||||
</ThemeProvider>
|
||||
</NavigationThemeProvider>
|
||||
</GluestackUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<ThemeProvider>
|
||||
<AppContent />
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
154
components/theme-example.tsx
Normal file
154
components/theme-example.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Example component demonstrating theme usage
|
||||
* Shows different ways to use the theme system
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useThemeColor } from "@/hooks/use-theme-color";
|
||||
|
||||
export function ThemeExampleComponent() {
|
||||
const { colors, styles, utils } = useAppTheme();
|
||||
|
||||
// Example of using useThemeColor hook
|
||||
const customTextColor = useThemeColor({}, "textSecondary");
|
||||
const customBackgroundColor = useThemeColor({}, "surfaceSecondary");
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<ThemedView style={styles.surface}>
|
||||
<ThemedText type="title">Theme Examples</ThemedText>
|
||||
|
||||
{/* Using themed components */}
|
||||
<ThemedText type="subtitle">Themed Components</ThemedText>
|
||||
<ThemedView style={styles.card}>
|
||||
<ThemedText>This is a themed text</ThemedText>
|
||||
<ThemedText type="defaultSemiBold">
|
||||
This is bold themed text
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
{/* Using theme colors directly */}
|
||||
<ThemedText type="subtitle">Direct Color Usage</ThemedText>
|
||||
<View
|
||||
style={[styles.card, { borderColor: colors.primary, borderWidth: 2 }]}
|
||||
>
|
||||
<Text style={{ color: colors.text, fontSize: 16 }}>
|
||||
Using colors.text directly
|
||||
</Text>
|
||||
<Text
|
||||
style={{ color: colors.primary, fontSize: 14, fontWeight: "600" }}
|
||||
>
|
||||
Primary color text
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Using pre-styled components */}
|
||||
<ThemedText type="subtitle">Pre-styled Components</ThemedText>
|
||||
<View style={styles.card}>
|
||||
<TouchableOpacity style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>Primary Button</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.secondaryButton}>
|
||||
<Text style={styles.secondaryButtonText}>Secondary Button</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Status containers */}
|
||||
<ThemedText type="subtitle">Status Indicators</ThemedText>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.successContainer}>
|
||||
<Text style={{ color: colors.success, fontWeight: "600" }}>
|
||||
Success Message
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.warningContainer}>
|
||||
<Text style={{ color: colors.warning, fontWeight: "600" }}>
|
||||
Warning Message
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={{ color: colors.error, fontWeight: "600" }}>
|
||||
Error Message
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Using opacity colors */}
|
||||
<ThemedText type="subtitle">Opacity Colors</ThemedText>
|
||||
<View style={styles.card}>
|
||||
<View
|
||||
style={[
|
||||
styles.surface,
|
||||
{ backgroundColor: utils.getOpacityColor("primary", 0.1) },
|
||||
]}
|
||||
>
|
||||
<Text style={{ color: colors.primary }}>
|
||||
Primary with 10% opacity background
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.surface,
|
||||
{ backgroundColor: utils.getOpacityColor("error", 0.2) },
|
||||
]}
|
||||
>
|
||||
<Text style={{ color: colors.error }}>
|
||||
Error with 20% opacity background
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Theme utilities */}
|
||||
<ThemedText type="subtitle">Theme Utilities</ThemedText>
|
||||
<View style={styles.card}>
|
||||
<Text style={{ color: colors.text }}>
|
||||
Is Dark Mode: {utils.isDark ? "Yes" : "No"}
|
||||
</Text>
|
||||
<Text style={{ color: colors.text }}>
|
||||
Is Light Mode: {utils.isLight ? "Yes" : "No"}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, { marginTop: 10 }]}
|
||||
onPress={utils.toggleTheme}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>
|
||||
Toggle Theme (Light/Dark)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Custom themed component example */}
|
||||
<ThemedText type="subtitle">Custom Component</ThemedText>
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: customBackgroundColor,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: customTextColor,
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Custom component using useThemeColor
|
||||
</Text>
|
||||
</View>
|
||||
</ThemedView>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
109
components/theme-toggle.tsx
Normal file
109
components/theme-toggle.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Theme Toggle Component for switching between light, dark, and system themes
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
interface ThemeToggleProps {
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ style }: ThemeToggleProps) {
|
||||
const { themeMode, setThemeMode, colors } = useThemeContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const themeOptions = [
|
||||
{
|
||||
mode: "light" as const,
|
||||
label: t("common.theme_light"),
|
||||
icon: "sunny-outline" as const,
|
||||
},
|
||||
{
|
||||
mode: "dark" as const,
|
||||
label: t("common.theme_dark"),
|
||||
icon: "moon-outline" as const,
|
||||
},
|
||||
{
|
||||
mode: "system" as const,
|
||||
label: t("common.theme_system"),
|
||||
icon: "phone-portrait-outline" as const,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, style, { backgroundColor: colors.surface }]}
|
||||
>
|
||||
<ThemedText style={styles.title}>{t("common.theme")}</ThemedText>
|
||||
<View style={styles.optionsContainer}>
|
||||
{themeOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option.mode}
|
||||
style={[
|
||||
styles.option,
|
||||
{
|
||||
backgroundColor:
|
||||
themeMode === option.mode
|
||||
? colors.primary
|
||||
: colors.backgroundSecondary,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
]}
|
||||
onPress={() => setThemeMode(option.mode)}
|
||||
>
|
||||
<Ionicons
|
||||
name={option.icon}
|
||||
size={20}
|
||||
color={themeMode === option.mode ? "#fff" : colors.icon}
|
||||
/>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.optionText,
|
||||
{ color: themeMode === option.mode ? "#fff" : colors.text },
|
||||
]}
|
||||
>
|
||||
{option.label}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
marginVertical: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
},
|
||||
optionsContainer: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
},
|
||||
option: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
gap: 6,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useTrip } from "@/state/use-trip";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { Animated, Text, TouchableOpacity, View } from "react-native";
|
||||
import CrewDetailModal from "./modal/CrewDetailModal";
|
||||
import styles from "./style/CrewListTable.styles";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { createTableStyles } from "./ThemedTable";
|
||||
|
||||
const CrewListTable: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
@@ -15,6 +17,9 @@ const CrewListTable: React.FC = () => {
|
||||
null
|
||||
);
|
||||
const { t } = useI18n();
|
||||
const { colorScheme } = useAppTheme();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
|
||||
|
||||
const { trip } = useTrip();
|
||||
|
||||
@@ -60,7 +65,7 @@ const CrewListTable: React.FC = () => {
|
||||
<IconSymbol
|
||||
name={collapsed ? "chevron.down" : "chevron.up"}
|
||||
size={16}
|
||||
color="#000"
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useTrip } from "@/state/use-trip";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { Animated, Text, TouchableOpacity, View } from "react-native";
|
||||
import styles from "./style/FishingToolsTable.styles";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { createTableStyles } from "./ThemedTable";
|
||||
|
||||
const FishingToolsTable: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [contentHeight, setContentHeight] = useState<number>(0);
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
const { t } = useI18n();
|
||||
const { colorScheme } = useAppTheme();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
|
||||
|
||||
const { trip } = useTrip();
|
||||
const data: Model.FishingGear[] = trip?.fishing_gears ?? [];
|
||||
@@ -38,7 +43,7 @@ const FishingToolsTable: React.FC = () => {
|
||||
<IconSymbol
|
||||
name={collapsed ? "chevron.down" : "chevron.up"}
|
||||
size={16}
|
||||
color="#000"
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useFishes } from "@/state/use-fish";
|
||||
import { useTrip } from "@/state/use-trip";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Animated, Text, TouchableOpacity, View } from "react-native";
|
||||
import CreateOrUpdateHaulModal from "./modal/CreateOrUpdateHaulModal";
|
||||
import styles from "./style/NetListTable.styles";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { createTableStyles } from "./ThemedTable";
|
||||
|
||||
const NetListTable: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
@@ -14,17 +16,16 @@ const NetListTable: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [selectedNet, setSelectedNet] = useState<Model.FishingLog | null>(null);
|
||||
const { t } = useI18n();
|
||||
const { colorScheme } = useAppTheme();
|
||||
const { colors } = useThemeContext();
|
||||
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
|
||||
|
||||
const { trip } = useTrip();
|
||||
const { fishSpecies, getFishSpecies } = useFishes();
|
||||
useEffect(() => {
|
||||
getFishSpecies();
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log("Trip thay đổi: ", trip?.fishing_logs?.length);
|
||||
// }, [trip]);
|
||||
// const data: Model.FishingLog[] = trip?.fishing_logs ?? [];
|
||||
|
||||
const handleToggle = () => {
|
||||
const toValue = collapsed ? contentHeight : 0;
|
||||
Animated.timing(animatedHeight, {
|
||||
@@ -60,7 +61,7 @@ const NetListTable: React.FC = () => {
|
||||
<IconSymbol
|
||||
name={collapsed ? "chevron.down" : "chevron.up"}
|
||||
size={16}
|
||||
color="#000"
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
29
components/tripInfo/ThemedTable.tsx
Normal file
29
components/tripInfo/ThemedTable.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Wrapper component to easily apply theme-aware table styles
|
||||
*/
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { createTableStyles } from "./style/createTableStyles";
|
||||
|
||||
interface ThemedTableProps extends ViewProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ThemedTable({ style, children, ...props }: ThemedTableProps) {
|
||||
const { colorScheme } = useAppTheme();
|
||||
const tableStyles = useMemo(
|
||||
() => createTableStyles(colorScheme),
|
||||
[colorScheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[tableStyles.container, style]} {...props}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export { createTableStyles };
|
||||
export type { TableStyles } from "./style/createTableStyles";
|
||||
@@ -1,14 +1,12 @@
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useAppTheme } from "@/hooks/use-app-theme";
|
||||
import { useTrip } from "@/state/use-trip";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Animated, Text, TouchableOpacity, View } from "react-native";
|
||||
import { createTableStyles } from "./style/createTableStyles";
|
||||
import TripCostDetailModal from "./modal/TripCostDetailModal";
|
||||
import styles from "./style/TripCostTable.styles";
|
||||
|
||||
// ---------------------------
|
||||
// 💰 Component chính
|
||||
// ---------------------------
|
||||
import React, { useRef, useState, useMemo } from "react";
|
||||
import { Animated, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
const TripCostTable: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
@@ -16,9 +14,13 @@ const TripCostTable: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
const { t } = useI18n();
|
||||
const { colorScheme } = useAppTheme();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const { trip } = useTrip();
|
||||
|
||||
const styles = useMemo(() => createTableStyles(colorScheme), [colorScheme]);
|
||||
|
||||
const data: Model.TripCost[] = trip?.trip_cost ?? [];
|
||||
const tongCong = data.reduce((sum, item) => sum + item.total_cost, 0);
|
||||
|
||||
@@ -54,19 +56,14 @@ const TripCostTable: React.FC = () => {
|
||||
>
|
||||
<Text style={styles.title}>{t("trip.costTable.title")}</Text>
|
||||
{collapsed && (
|
||||
<Text
|
||||
style={[
|
||||
styles.title,
|
||||
{ color: "#ff6600", fontWeight: "bold", marginLeft: 8 },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.totalCollapsed]}>
|
||||
{tongCong.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
<IconSymbol
|
||||
name={collapsed ? "chevron.down" : "chevron.up"}
|
||||
size={15}
|
||||
color="#000000"
|
||||
color={colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
},
|
||||
totalCollapsed: {
|
||||
color: "#ff6600",
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: "#eee",
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: "#fafafa",
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: "#111",
|
||||
textAlign: "center",
|
||||
},
|
||||
cellWrapper: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
right: {
|
||||
textAlign: "center",
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerText: {
|
||||
color: "#007bff",
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerTotal: {
|
||||
color: "#ff6600",
|
||||
fontWeight: "800",
|
||||
},
|
||||
linkText: {
|
||||
color: "#007AFF",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
},
|
||||
totalCollapsed: {
|
||||
color: "#ff6600",
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: "#eee",
|
||||
paddingLeft: 15,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: "#111",
|
||||
},
|
||||
left: {
|
||||
textAlign: "left",
|
||||
},
|
||||
right: {
|
||||
textAlign: "center",
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: "#fafafa",
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerText: {
|
||||
color: "#007bff",
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerTotal: {
|
||||
color: "#ff6600",
|
||||
fontWeight: "800",
|
||||
},
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: "#eee",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 1,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
},
|
||||
totalCollapsed: {
|
||||
color: "#ff6600",
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: "#eee",
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: "#fafafa",
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: "#111",
|
||||
textAlign: "center",
|
||||
},
|
||||
sttCell: {
|
||||
flex: 0.3,
|
||||
fontSize: 15,
|
||||
color: "#111",
|
||||
textAlign: "center",
|
||||
paddingLeft: 10,
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
statusContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#2ecc71",
|
||||
marginRight: 6,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 15,
|
||||
color: "#4a90e2",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
margin: 16,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#fff",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderColor: "#ddd",
|
||||
paddingVertical: 8,
|
||||
paddingLeft: 15,
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 15,
|
||||
},
|
||||
left: {
|
||||
textAlign: "left",
|
||||
},
|
||||
right: {
|
||||
color: "#ff6600",
|
||||
fontWeight: "600",
|
||||
},
|
||||
header: {
|
||||
backgroundColor: "#f8f8f8",
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: 10,
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
footer: {
|
||||
marginTop: 6,
|
||||
},
|
||||
footerText: {
|
||||
fontWeight: "600",
|
||||
color: "#007bff",
|
||||
},
|
||||
total: {
|
||||
color: "#ff6600",
|
||||
fontWeight: "700",
|
||||
},
|
||||
viewDetailButton: {
|
||||
marginTop: 12,
|
||||
paddingVertical: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
viewDetailText: {
|
||||
color: "#007AFF",
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
175
components/tripInfo/style/createTableStyles.ts
Normal file
175
components/tripInfo/style/createTableStyles.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Colors } from "@/constants/theme";
|
||||
|
||||
export type ColorScheme = "light" | "dark";
|
||||
|
||||
export function createTableStyles(colorScheme: ColorScheme) {
|
||||
const colors = Colors[colorScheme];
|
||||
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginVertical: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
shadowColor: colors.text,
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
color: colors.text,
|
||||
},
|
||||
totalCollapsed: {
|
||||
color: colors.warning,
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.separator,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
left: {
|
||||
textAlign: "left",
|
||||
},
|
||||
rowHorizontal: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.separator,
|
||||
paddingLeft: 15,
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
headerCell: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
},
|
||||
headerCellLeft: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: colors.text,
|
||||
textAlign: "left",
|
||||
},
|
||||
cell: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
},
|
||||
cellLeft: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
textAlign: "left",
|
||||
},
|
||||
cellRight: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
textAlign: "right",
|
||||
},
|
||||
cellWrapper: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerText: {
|
||||
fontWeight: "600",
|
||||
color: colors.text,
|
||||
},
|
||||
footerText: {
|
||||
color: colors.primary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerTotal: {
|
||||
color: colors.warning,
|
||||
fontWeight: "800",
|
||||
},
|
||||
sttCell: {
|
||||
flex: 0.3,
|
||||
fontSize: 15,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
paddingLeft: 10,
|
||||
},
|
||||
statusContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.success,
|
||||
marginRight: 6,
|
||||
},
|
||||
statusDotPending: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.warning,
|
||||
marginRight: 6,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 15,
|
||||
color: colors.primary,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
linkText: {
|
||||
color: colors.primary,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
viewDetailButton: {
|
||||
marginTop: 12,
|
||||
paddingVertical: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
viewDetailText: {
|
||||
color: colors.primary,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
total: {
|
||||
color: colors.warning,
|
||||
fontWeight: "700",
|
||||
},
|
||||
right: {
|
||||
color: colors.warning,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footerRow: {
|
||||
marginTop: 6,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type TableStyles = ReturnType<typeof createTableStyles>;
|
||||
@@ -3,51 +3,82 @@
|
||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
const tintColorLight = "#0a7ea4";
|
||||
const tintColorDark = "#fff";
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: '#11181C',
|
||||
background: '#fff',
|
||||
text: "#11181C",
|
||||
textSecondary: "#687076",
|
||||
background: "#fff",
|
||||
backgroundSecondary: "#f5f5f5",
|
||||
surface: "#ffffff",
|
||||
surfaceSecondary: "#f8f9fa",
|
||||
tint: tintColorLight,
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
primary: "#007AFF",
|
||||
secondary: "#5AC8FA",
|
||||
success: "#34C759",
|
||||
warning: "#ff6600",
|
||||
error: "#FF3B30",
|
||||
icon: "#687076",
|
||||
iconSecondary: "#8E8E93",
|
||||
border: "#C6C6C8",
|
||||
separator: "#E5E5E7",
|
||||
tabIconDefault: "#687076",
|
||||
tabIconSelected: tintColorLight,
|
||||
card: "#ffffff",
|
||||
notification: "#FF3B30",
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
background: '#151718',
|
||||
text: "#ECEDEE",
|
||||
textSecondary: "#8E8E93",
|
||||
background: "#000000",
|
||||
backgroundSecondary: "#1C1C1E",
|
||||
surface: "#1C1C1E",
|
||||
surfaceSecondary: "#2C2C2E",
|
||||
tint: tintColorDark,
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
primary: "#0A84FF",
|
||||
secondary: "#64D2FF",
|
||||
success: "#30D158",
|
||||
warning: "#ff6600",
|
||||
error: "#FF453A",
|
||||
icon: "#8E8E93",
|
||||
iconSecondary: "#636366",
|
||||
border: "#38383A",
|
||||
separator: "#38383A",
|
||||
tabIconDefault: "#8E8E93",
|
||||
tabIconSelected: tintColorDark,
|
||||
card: "#1C1C1E",
|
||||
notification: "#FF453A",
|
||||
},
|
||||
};
|
||||
|
||||
export type ColorName = keyof typeof Colors.light;
|
||||
|
||||
export const Fonts = Platform.select({
|
||||
ios: {
|
||||
/** iOS `UIFontDescriptorSystemDesignDefault` */
|
||||
sans: 'system-ui',
|
||||
sans: "system-ui",
|
||||
/** iOS `UIFontDescriptorSystemDesignSerif` */
|
||||
serif: 'ui-serif',
|
||||
serif: "ui-serif",
|
||||
/** iOS `UIFontDescriptorSystemDesignRounded` */
|
||||
rounded: 'ui-rounded',
|
||||
rounded: "ui-rounded",
|
||||
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
|
||||
mono: 'ui-monospace',
|
||||
mono: "ui-monospace",
|
||||
},
|
||||
default: {
|
||||
sans: 'normal',
|
||||
serif: 'serif',
|
||||
rounded: 'normal',
|
||||
mono: 'monospace',
|
||||
sans: "normal",
|
||||
serif: "serif",
|
||||
rounded: "normal",
|
||||
mono: "monospace",
|
||||
},
|
||||
web: {
|
||||
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
||||
serif: "Georgia, 'Times New Roman', serif",
|
||||
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
|
||||
rounded:
|
||||
"'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
|
||||
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
},
|
||||
});
|
||||
|
||||
163
hooks/use-app-theme.ts
Normal file
163
hooks/use-app-theme.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Custom hook for easy theme access throughout the app
|
||||
* Provides styled components and theme utilities
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { StyleSheet, TextStyle, ViewStyle } from "react-native";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
export function useAppTheme() {
|
||||
const { colors, colorScheme, themeMode, setThemeMode, getColor } =
|
||||
useThemeContext();
|
||||
|
||||
// Common styled components
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
StyleSheet.create({
|
||||
// Container styles
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
} as ViewStyle,
|
||||
|
||||
surface: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
} as ViewStyle,
|
||||
|
||||
card: {
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
shadowColor: colors.text,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
} as ViewStyle,
|
||||
|
||||
// Button styles
|
||||
primaryButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
} as ViewStyle,
|
||||
|
||||
secondaryButton: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
} as ViewStyle,
|
||||
|
||||
// Text styles
|
||||
primaryButtonText: {
|
||||
color: "#ffffff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
} as TextStyle,
|
||||
|
||||
secondaryButtonText: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
} as TextStyle,
|
||||
|
||||
// Input styles
|
||||
textInput: {
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
color: colors.text,
|
||||
} as ViewStyle & TextStyle,
|
||||
|
||||
// Separator
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: colors.separator,
|
||||
} as ViewStyle,
|
||||
|
||||
// Status styles
|
||||
successContainer: {
|
||||
backgroundColor: `${colors.success}20`,
|
||||
borderColor: colors.success,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
} as ViewStyle,
|
||||
|
||||
warningContainer: {
|
||||
backgroundColor: `${colors.warning}20`,
|
||||
borderColor: colors.warning,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
} as ViewStyle,
|
||||
|
||||
errorContainer: {
|
||||
backgroundColor: `${colors.error}20`,
|
||||
borderColor: colors.error,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
} as ViewStyle,
|
||||
}),
|
||||
[colors]
|
||||
);
|
||||
|
||||
// Theme utilities
|
||||
const utils = useMemo(
|
||||
() => ({
|
||||
// Get opacity color
|
||||
getOpacityColor: (
|
||||
colorName: keyof typeof colors,
|
||||
opacity: number = 0.1
|
||||
) => {
|
||||
const color = colors[colorName];
|
||||
const hex = color.replace("#", "");
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
},
|
||||
|
||||
// Check if current theme is dark
|
||||
isDark: colorScheme === "dark",
|
||||
|
||||
// Check if current theme is light
|
||||
isLight: colorScheme === "light",
|
||||
|
||||
// Toggle between light and dark (ignoring system)
|
||||
toggleTheme: () => {
|
||||
const newMode = colorScheme === "dark" ? "light" : "dark";
|
||||
setThemeMode(newMode);
|
||||
},
|
||||
}),
|
||||
[colors, colorScheme, setThemeMode]
|
||||
);
|
||||
|
||||
return {
|
||||
colors,
|
||||
styles,
|
||||
utils,
|
||||
colorScheme,
|
||||
themeMode,
|
||||
setThemeMode,
|
||||
getColor,
|
||||
};
|
||||
}
|
||||
|
||||
export type AppTheme = ReturnType<typeof useAppTheme>;
|
||||
@@ -3,19 +3,19 @@
|
||||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/theme';
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
import { ColorName } from "@/constants/theme";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
colorName: ColorName
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
const { colorScheme, getColor } = useThemeContext();
|
||||
const colorFromProps = props[colorScheme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
return getColor(colorName);
|
||||
}
|
||||
}
|
||||
|
||||
173
hooks/use-theme-context.tsx
Normal file
173
hooks/use-theme-context.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Theme Context Hook for managing app-wide theme state
|
||||
* Supports Light, Dark, and System (automatic) modes
|
||||
*
|
||||
* IMPORTANT: Requires expo-system-ui plugin in app.json for Android support
|
||||
*/
|
||||
|
||||
import { Colors, ColorName } from "@/constants/theme";
|
||||
import { getStorageItem, setStorageItem } from "@/utils/storage";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
useColorScheme as useSystemColorScheme,
|
||||
Appearance,
|
||||
} from "react-native";
|
||||
|
||||
type ThemeMode = "light" | "dark" | "system";
|
||||
type ColorScheme = "light" | "dark";
|
||||
|
||||
interface ThemeContextType {
|
||||
themeMode: ThemeMode;
|
||||
colorScheme: ColorScheme;
|
||||
colors: typeof Colors.light;
|
||||
setThemeMode: (mode: ThemeMode) => Promise<void>;
|
||||
getColor: (colorName: ColorName) => string;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
const THEME_STORAGE_KEY = "theme_mode";
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
// State để force re-render khi system theme thay đổi
|
||||
const [systemTheme, setSystemTheme] = useState<ColorScheme>(() => {
|
||||
const current = Appearance.getColorScheme();
|
||||
console.log("[Theme] Initial system theme:", current);
|
||||
return current === "dark" ? "dark" : "light";
|
||||
});
|
||||
|
||||
// State lưu user's choice (light/dark/system)
|
||||
const [themeMode, setThemeModeState] = useState<ThemeMode>("system");
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
// Listen vào system theme changes - đăng ký ngay từ đầu
|
||||
useEffect(() => {
|
||||
console.log("[Theme] Registering appearance listener");
|
||||
|
||||
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
const newScheme = colorScheme === "dark" ? "dark" : "light";
|
||||
console.log(
|
||||
"[Theme] System theme changed to:",
|
||||
newScheme,
|
||||
"at",
|
||||
new Date().toLocaleTimeString()
|
||||
);
|
||||
setSystemTheme(newScheme);
|
||||
});
|
||||
|
||||
// Double check current theme khi mount
|
||||
const currentScheme = Appearance.getColorScheme();
|
||||
const current = currentScheme === "dark" ? "dark" : "light";
|
||||
if (current !== systemTheme) {
|
||||
console.log("[Theme] Syncing system theme on mount:", current);
|
||||
setSystemTheme(current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
console.log("[Theme] Removing appearance listener");
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Xác định colorScheme cuối cùng
|
||||
const colorScheme: ColorScheme =
|
||||
themeMode === "system" ? systemTheme : themeMode;
|
||||
|
||||
const colors = Colors[colorScheme];
|
||||
|
||||
// Log để debug
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"[Theme] Current state - Mode:",
|
||||
themeMode,
|
||||
"| Scheme:",
|
||||
colorScheme,
|
||||
"| System:",
|
||||
systemTheme
|
||||
);
|
||||
}, [themeMode, colorScheme, systemTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadThemeMode = async () => {
|
||||
try {
|
||||
const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
|
||||
if (
|
||||
savedThemeMode &&
|
||||
["light", "dark", "system"].includes(savedThemeMode)
|
||||
) {
|
||||
setThemeModeState(savedThemeMode as ThemeMode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load theme mode:", error);
|
||||
} finally {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
};
|
||||
loadThemeMode();
|
||||
}, []);
|
||||
|
||||
const setThemeMode = async (mode: ThemeMode) => {
|
||||
try {
|
||||
setThemeModeState(mode);
|
||||
await setStorageItem(THEME_STORAGE_KEY, mode);
|
||||
console.log("[Theme] Changed to:", mode);
|
||||
} catch (error) {
|
||||
console.warn("Failed to save theme mode:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getColor = (colorName: ColorName): string => {
|
||||
return colors[colorName] || colors.text;
|
||||
};
|
||||
|
||||
// Chờ theme load xong trước khi render
|
||||
if (!isLoaded) {
|
||||
// Render với default theme (system) khi đang load
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
themeMode: "system",
|
||||
colorScheme: systemTheme,
|
||||
colors: Colors[systemTheme],
|
||||
setThemeMode: async () => {},
|
||||
getColor: (colorName: ColorName) =>
|
||||
Colors[systemTheme][colorName] || Colors[systemTheme].text,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const value: ThemeContextType = {
|
||||
themeMode,
|
||||
colorScheme,
|
||||
colors,
|
||||
setThemeMode,
|
||||
getColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useThemeContext(): ThemeContextType {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useThemeContext must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Legacy hook cho backward compatibility
|
||||
export function useColorScheme(): ColorScheme {
|
||||
const { colorScheme } = useThemeContext();
|
||||
return colorScheme;
|
||||
}
|
||||
@@ -18,7 +18,11 @@
|
||||
"warning": "Warning",
|
||||
"language": "Language",
|
||||
"language_vi": "Vietnamese",
|
||||
"language_en": "English"
|
||||
"language_en": "English",
|
||||
"theme": "Theme",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"theme_system": "System"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Monitor",
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
"warning": "Cảnh báo",
|
||||
"language": "Ngôn ngữ",
|
||||
"language_vi": "Tiếng Việt",
|
||||
"language_en": "Tiếng Anh"
|
||||
"language_en": "Tiếng Anh",
|
||||
"theme": "Giao diện",
|
||||
"theme_light": "Sáng",
|
||||
"theme_dark": "Tối",
|
||||
"theme_system": "Hệ thống"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Giám sát",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user