Base dark theme implementation
This commit is contained in:
1
app.json
1
app.json
@@ -4,6 +4,7 @@
|
|||||||
"slug": "imexmobile",
|
"slug": "imexmobile",
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"scheme": "imex-mobile-scheme",
|
"scheme": "imex-mobile-scheme",
|
||||||
|
"userInterfaceStyle": "automatic",
|
||||||
"extra": {
|
"extra": {
|
||||||
"expover": "1",
|
"expover": "1",
|
||||||
"eas": {
|
"eas": {
|
||||||
|
|||||||
114
app/_layout.tsx
114
app/_layout.tsx
@@ -2,6 +2,11 @@ import { checkUserSession } from "@/redux/user/user.actions";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
|
import {
|
||||||
|
DarkTheme,
|
||||||
|
DefaultTheme,
|
||||||
|
ThemeProvider,
|
||||||
|
} from "@react-navigation/native";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
@@ -9,7 +14,7 @@ import {
|
|||||||
NativeTabs,
|
NativeTabs,
|
||||||
VectorIcon,
|
VectorIcon,
|
||||||
} from "expo-router/unstable-native-tabs";
|
} from "expo-router/unstable-native-tabs";
|
||||||
import { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, Platform, View } from "react-native";
|
import { ActivityIndicator, Platform, View } from "react-native";
|
||||||
import { Provider as PaperProvider } from "react-native-paper";
|
import { Provider as PaperProvider } from "react-native-paper";
|
||||||
@@ -17,45 +22,55 @@ import { connect, Provider } from "react-redux";
|
|||||||
import { PersistGate } from "redux-persist/integration/react";
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { client } from "../graphql/client";
|
import { client } from "../graphql/client";
|
||||||
|
import { useTheme as usePaperTheme } from "../hooks/useTheme";
|
||||||
import { persistor, store } from "../redux/store";
|
import { persistor, store } from "../redux/store";
|
||||||
import "../translations/i18n";
|
import "../translations/i18n";
|
||||||
import theme from "../util/theme";
|
|
||||||
|
|
||||||
function AuthenticatedLayout() {
|
function AuthenticatedLayout() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const paperTheme = usePaperTheme();
|
||||||
return (
|
return (
|
||||||
<NativeTabs minimizeBehavior="onScrollDown" disableTransparentOnScrollEdge>
|
<ThemeProvider
|
||||||
<NativeTabs.Trigger name="jobs">
|
value={paperTheme.theme === "dark" ? DarkTheme : DefaultTheme}
|
||||||
<Label>{t("joblist.labels.activejobs")}</Label>
|
>
|
||||||
|
<NativeTabs
|
||||||
|
minimizeBehavior="onScrollDown"
|
||||||
|
disableTransparentOnScrollEdge
|
||||||
|
>
|
||||||
|
<NativeTabs.Trigger name="jobs">
|
||||||
|
<Label>{t("joblist.labels.activejobs")}</Label>
|
||||||
|
|
||||||
{Platform.select({
|
{Platform.select({
|
||||||
ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
|
ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
|
||||||
android: (
|
android: (
|
||||||
<Icon
|
<Icon
|
||||||
src={<VectorIcon family={MaterialIcons} name="checklist" />}
|
src={<VectorIcon family={MaterialIcons} name="checklist" />}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="settings">
|
<NativeTabs.Trigger name="settings">
|
||||||
{Platform.select({
|
{Platform.select({
|
||||||
ios: <Icon sf="gear" drawable="custom_android_drawable" />,
|
ios: <Icon sf="gear" drawable="custom_android_drawable" />,
|
||||||
android: (
|
android: (
|
||||||
<Icon src={<VectorIcon family={MaterialIcons} name="settings" />} />
|
<Icon
|
||||||
),
|
src={<VectorIcon family={MaterialIcons} name="settings" />}
|
||||||
})}
|
/>
|
||||||
<Label>{t("settings.titles.settings")}</Label>
|
),
|
||||||
</NativeTabs.Trigger>
|
})}
|
||||||
<NativeTabs.Trigger name="search" role="search">
|
<Label>{t("settings.titles.settings")}</Label>
|
||||||
{Platform.select({
|
</NativeTabs.Trigger>
|
||||||
//ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
|
<NativeTabs.Trigger name="search" role="search">
|
||||||
android: (
|
{Platform.select({
|
||||||
<Icon src={<VectorIcon family={MaterialIcons} name="search" />} />
|
//ios: <Icon sf="checklist" drawable="custom_android_drawable" />,
|
||||||
),
|
android: (
|
||||||
})}
|
<Icon src={<VectorIcon family={MaterialIcons} name="search" />} />
|
||||||
<Label>Search</Label>
|
),
|
||||||
</NativeTabs.Trigger>
|
})}
|
||||||
</NativeTabs>
|
<Label>Search</Label>
|
||||||
|
</NativeTabs.Trigger>
|
||||||
|
</NativeTabs>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,21 +94,38 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
});
|
});
|
||||||
function AppContent({ currentUser, checkUserSession, bodyshop }) {
|
function AppContent({ currentUser, checkUserSession, bodyshop }: any) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkUserSession();
|
checkUserSession();
|
||||||
}, []);
|
}, [checkUserSession]);
|
||||||
|
|
||||||
if (currentUser.authorized === null) {
|
if (currentUser.authorized === null) {
|
||||||
return <LoadingLayout />;
|
return (
|
||||||
|
<ThemedLayout>
|
||||||
|
<LoadingLayout />
|
||||||
|
</ThemedLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (currentUser.authorized) {
|
if (currentUser.authorized) {
|
||||||
return <AuthenticatedLayout />;
|
return (
|
||||||
|
<ThemedLayout>
|
||||||
|
<AuthenticatedLayout />
|
||||||
|
</ThemedLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <UnauthenticatedLayout />;
|
return (
|
||||||
|
<ThemedLayout>
|
||||||
|
<UnauthenticatedLayout />
|
||||||
|
</ThemedLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemedLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const themeToApply = usePaperTheme();
|
||||||
|
return <PaperProvider theme={themeToApply}>{children}</PaperProvider>;
|
||||||
}
|
}
|
||||||
const ConnectedAppContent = connect(
|
const ConnectedAppContent = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
@@ -105,9 +137,7 @@ export default function AppLayout() {
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate persistor={persistor}>
|
<PersistGate persistor={persistor}>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<PaperProvider theme={theme}>
|
<ConnectedAppContent />
|
||||||
<ConnectedAppContent />
|
|
||||||
</PaperProvider>
|
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Stack, useRouter } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function JobsStack() {
|
function JobsStack() {
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
@@ -17,15 +16,6 @@ function JobsStack() {
|
|||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
title: t("joblist.titles.jobtab"),
|
title: t("joblist.titles.jobtab"),
|
||||||
// headerSearchBarOptions: {
|
|
||||||
// placement: "automatic",
|
|
||||||
// placeholder: "Search",
|
|
||||||
// onChangeText: (event) => {
|
|
||||||
// router.setParams({
|
|
||||||
// search: event?.nativeEvent?.text,
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Text, View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { Text } from "react-native-paper";
|
||||||
|
|
||||||
export default function DataLabelComponent({
|
export default function DataLabelComponent({
|
||||||
label,
|
label,
|
||||||
@@ -17,7 +18,7 @@ export default function DataLabelComponent({
|
|||||||
const { key, ...rest } = restProps;
|
const { key, ...rest } = restProps;
|
||||||
return (
|
return (
|
||||||
<View key={key} {...rest} style={{ margin: 4, ...restProps.style }}>
|
<View key={key} {...rest} style={{ margin: 4, ...restProps.style }}>
|
||||||
<Text style={{ color: "slategray" }}>{label}</Text>
|
<Text>{label}</Text>
|
||||||
<Text>{theContent}</Text>
|
<Text>{theContent}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Text } from "react-native";
|
|
||||||
import { Button, Card } from "react-native-paper";
|
import { Button, Card, Text } from "react-native-paper";
|
||||||
|
|
||||||
export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
|
export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -14,11 +14,11 @@ export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
|
|||||||
error ||
|
error ||
|
||||||
"An unknown error has occured."}
|
"An unknown error has occured."}
|
||||||
</Text>
|
</Text>
|
||||||
{onDismiss ? (
|
{onDismiss ? (
|
||||||
<Card.Actions>
|
<Card.Actions>
|
||||||
<Button onPress={onDismiss}>{t("general.labels.dismiss")}</Button>
|
<Button onPress={onDismiss}>{t("general.labels.dismiss")}</Button>
|
||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
) : null}
|
) : null}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import ImageView from "react-native-image-viewing";
|
import ImageView from "react-native-image-viewing";
|
||||||
import { ActivityIndicator } from "react-native-paper";
|
import { ActivityIndicator, Text } from "react-native-paper";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import env from "../../env";
|
import env from "../../env";
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useGlobalSearchParams } from "expo-router";
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FlatList, RefreshControl, Text, View } from "react-native";
|
import { FlatList, RefreshControl, View } from "react-native";
|
||||||
import { ActivityIndicator, Card } from "react-native-paper";
|
import { ActivityIndicator, Card, Text } from "react-native-paper";
|
||||||
import ErrorDisplay from "../error/error-display";
|
import ErrorDisplay from "../error/error-display";
|
||||||
|
|
||||||
export default function JobNotes() {
|
export default function JobNotes() {
|
||||||
|
|||||||
@@ -3,14 +3,8 @@ import { useQuery } from "@apollo/client";
|
|||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
|
||||||
RefreshControl,
|
import { ActivityIndicator, Card, Text, useTheme } from "react-native-paper";
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { ActivityIndicator, Card, useTheme } from "react-native-paper";
|
|
||||||
import DataLabelComponent from "../data-label/data-label";
|
import DataLabelComponent from "../data-label/data-label";
|
||||||
|
|
||||||
export default function JobTombstone() {
|
export default function JobTombstone() {
|
||||||
@@ -82,14 +76,9 @@ export default function JobTombstone() {
|
|||||||
/>
|
/>
|
||||||
<DataLabelComponent
|
<DataLabelComponent
|
||||||
label={t("objects.jobs.fields.vehicle")}
|
label={t("objects.jobs.fields.vehicle")}
|
||||||
content={
|
content={`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||||
<View>
|
job.v_model_desc || ""
|
||||||
<Text>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
} - ${job.v_vin}`}
|
||||||
job.v_model_desc || ""
|
|
||||||
}`}</Text>
|
|
||||||
<Text>{job.v_vin}</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={localStyles.twoColumnCardColumn}>
|
<View style={localStyles.twoColumnCardColumn}>
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ function JobListItemComponent({ openImagePicker, item }) {
|
|||||||
style={[
|
style={[
|
||||||
styles.glassCard,
|
styles.glassCard,
|
||||||
{
|
{
|
||||||
backgroundColor: theme.dark
|
backgroundColor: theme.colors.primaryContainer,
|
||||||
? "rgba(30,30,30,0.55)"
|
|
||||||
: "rgba(255,255,255,0.55)",
|
|
||||||
borderColor: theme.colors.outlineVariant,
|
borderColor: theme.colors.outlineVariant,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Button, Card, Divider, List, Text } from "react-native-paper";
|
|||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { ThemeSelector } from "../theme-selector/theme-selector";
|
||||||
import UploadDeleteSwitch from "./upload-delete-switch";
|
import UploadDeleteSwitch from "./upload-delete-switch";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -24,6 +25,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(Tab);
|
|||||||
|
|
||||||
function Tab({ bodyshop, currentUser }) {
|
function Tab({ bodyshop, currentUser }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleClearStorage = () => {
|
const handleClearStorage = () => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Clear Local Cache",
|
"Clear Local Cache",
|
||||||
@@ -47,7 +49,7 @@ function Tab({ bodyshop, currentUser }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
<Text variant="headlineMedium" style={styles.title}>
|
<Text variant="headlineMedium" style={styles.title}>
|
||||||
Settings
|
Settings
|
||||||
@@ -59,7 +61,6 @@ function Tab({ bodyshop, currentUser }) {
|
|||||||
<List.Section>
|
<List.Section>
|
||||||
<View style={styles.inlineRow}>
|
<View style={styles.inlineRow}>
|
||||||
<Text style={styles.switchLabel}>
|
<Text style={styles.switchLabel}>
|
||||||
{" "}
|
|
||||||
{t("mediabrowser.labels.deleteafterupload")}
|
{t("mediabrowser.labels.deleteafterupload")}
|
||||||
</Text>
|
</Text>
|
||||||
<UploadDeleteSwitch />
|
<UploadDeleteSwitch />
|
||||||
@@ -97,6 +98,18 @@ function Tab({ bodyshop, currentUser }) {
|
|||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card style={styles.section}>
|
||||||
|
<Card.Title title="Theme" />
|
||||||
|
<Card.Content>
|
||||||
|
<List.Section>
|
||||||
|
<View style={styles.inlineRow}>
|
||||||
|
<ThemeSelector />
|
||||||
|
</View>
|
||||||
|
</List.Section>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Actions></Card.Actions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card style={styles.section}>
|
<Card style={styles.section}>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Text style={styles.paragraph}>
|
<Text style={styles.paragraph}>
|
||||||
|
|||||||
37
components/theme-selector/theme-selector.tsx
Normal file
37
components/theme-selector/theme-selector.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { setTheme } from "@/redux/user/user.actions";
|
||||||
|
import { selectTheme } from "@/redux/user/user.selectors";
|
||||||
|
import React from "react";
|
||||||
|
import { SegmentedButtons } from "react-native-paper";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example component showing how to use the useTheme hook
|
||||||
|
* and how to dispatch theme changes
|
||||||
|
*/
|
||||||
|
export const ThemeSelector = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentTheme = useSelector(selectTheme);
|
||||||
|
|
||||||
|
const handleThemeChange = (theme: "light" | "dark" | "system") => {
|
||||||
|
dispatch(setTheme(theme));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentedButtons
|
||||||
|
value={currentTheme}
|
||||||
|
onValueChange={handleThemeChange}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
value: "light",
|
||||||
|
label: "Light",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "dark",
|
||||||
|
label: "Dark",
|
||||||
|
},
|
||||||
|
{ value: "system", label: "System" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { clearUploadError } from "@/redux/photos/photos.actions";
|
import { clearUploadError } from "@/redux/photos/photos.actions";
|
||||||
import theme from "@/util/theme";
|
|
||||||
import { formatBytes } from "@/util/uploadUtils";
|
import { formatBytes } from "@/util/uploadUtils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, View } from "react-native";
|
import { StyleSheet, View } from "react-native";
|
||||||
@@ -92,7 +91,7 @@ const styles = StyleSheet.create({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
marginLeft: 12,
|
marginLeft: 12,
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
backgroundColor: theme.colors.elevation.level3,
|
//backgroundColor: theme.colors.elevation.level3,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
|
|||||||
1
hooks/index.ts
Normal file
1
hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { useTheme } from "./useTheme";
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { useColorScheme } from 'react-native';
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useColorScheme as useRNColorScheme } from 'react-native';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To support static rendering, this value needs to be re-calculated on the client side for web
|
|
||||||
*/
|
|
||||||
export function useColorScheme() {
|
|
||||||
const [hasHydrated, setHasHydrated] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setHasHydrated(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const colorScheme = useRNColorScheme();
|
|
||||||
|
|
||||||
if (hasHydrated) {
|
|
||||||
return colorScheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'light';
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Learn more about light and dark modes:
|
|
||||||
* https://docs.expo.dev/guides/color-schemes/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Colors } from '@/constants/theme';
|
|
||||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
|
||||||
|
|
||||||
export function useThemeColor(
|
|
||||||
props: { light?: string; dark?: string },
|
|
||||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
|
||||||
) {
|
|
||||||
const theme = useColorScheme() ?? 'light';
|
|
||||||
const colorFromProps = props[theme];
|
|
||||||
|
|
||||||
if (colorFromProps) {
|
|
||||||
return colorFromProps;
|
|
||||||
} else {
|
|
||||||
return Colors[theme][colorName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
hooks/useTheme.ts
Normal file
57
hooks/useTheme.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { selectTheme } from "@/redux/user/user.selectors";
|
||||||
|
import { darkTheme, lightTheme } from "@/util/theme";
|
||||||
|
import { useColorScheme } from "react-native";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
type ThemePreference = "light" | "dark" | "system";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that returns the appropriate theme based on user preference
|
||||||
|
*
|
||||||
|
* This hook automatically selects the correct theme (light or dark) based on:
|
||||||
|
* - User's explicit preference (light/dark)
|
||||||
|
* - System appearance when "system" is selected
|
||||||
|
* - Defaults to system theme if no preference is set
|
||||||
|
*
|
||||||
|
* @returns The theme object compatible with React Native Paper
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { useTheme } from "@/hooks/useTheme";
|
||||||
|
*
|
||||||
|
* const MyComponent = () => {
|
||||||
|
* const theme = useTheme();
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <View style={{ backgroundColor: theme.colors.background }}>
|
||||||
|
* <Text style={{ color: theme.colors.onBackground }}>
|
||||||
|
* Themed content
|
||||||
|
* </Text>
|
||||||
|
* </View>
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const useTheme = () => {
|
||||||
|
const userThemePreference: ThemePreference = useSelector(selectTheme);
|
||||||
|
const systemColorScheme = useColorScheme();
|
||||||
|
|
||||||
|
// Determine which theme to use based on user preference
|
||||||
|
let selectedTheme;
|
||||||
|
|
||||||
|
switch (userThemePreference) {
|
||||||
|
case "light":
|
||||||
|
selectedTheme = lightTheme;
|
||||||
|
break;
|
||||||
|
case "dark":
|
||||||
|
selectedTheme = darkTheme;
|
||||||
|
break;
|
||||||
|
case "system":
|
||||||
|
default:
|
||||||
|
// Use system preference when user selects "system" or as fallback
|
||||||
|
selectedTheme = systemColorScheme === "dark" ? darkTheme : lightTheme;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTheme;
|
||||||
|
};
|
||||||
@@ -98,3 +98,7 @@ export const validatePasswordResetFailure = (error) => ({
|
|||||||
type: UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE,
|
type: UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE,
|
||||||
payload: error,
|
payload: error,
|
||||||
});
|
});
|
||||||
|
export const setTheme = (theme) => ({
|
||||||
|
type: UserActionTypes.SET_THEME,
|
||||||
|
payload: theme,
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const INITIAL_STATE = {
|
|||||||
bodyshop: null,
|
bodyshop: null,
|
||||||
signingIn: false,
|
signingIn: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
theme: "system"
|
||||||
};
|
};
|
||||||
|
|
||||||
const userReducer = (state = INITIAL_STATE, action) => {
|
const userReducer = (state = INITIAL_STATE, action) => {
|
||||||
@@ -48,6 +49,8 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
|
|
||||||
case UserActionTypes.SET_SHOP_DETAILS:
|
case UserActionTypes.SET_SHOP_DETAILS:
|
||||||
return { ...state, bodyshop: action.payload };
|
return { ...state, bodyshop: action.payload };
|
||||||
|
case UserActionTypes.SET_THEME:
|
||||||
|
return { ...state, theme: action.payload };
|
||||||
case UserActionTypes.SIGN_IN_FAILURE:
|
case UserActionTypes.SIGN_IN_FAILURE:
|
||||||
case UserActionTypes.SIGN_OUT_FAILURE:
|
case UserActionTypes.SIGN_OUT_FAILURE:
|
||||||
case UserActionTypes.EMAIL_SIGN_UP_FAILURE:
|
case UserActionTypes.EMAIL_SIGN_UP_FAILURE:
|
||||||
|
|||||||
@@ -31,3 +31,8 @@ export const selectSigningIn = createSelector(
|
|||||||
[selectUser],
|
[selectUser],
|
||||||
(user) => user.signingIn
|
(user) => user.signingIn
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectTheme = createSelector(
|
||||||
|
[selectUser],
|
||||||
|
(user) => user.theme
|
||||||
|
);
|
||||||
@@ -26,5 +26,6 @@ const UserActionTypes = {
|
|||||||
VALIDATE_PASSWORD_RESET_START: "VALIDATE_PASSWORD_RESET_START",
|
VALIDATE_PASSWORD_RESET_START: "VALIDATE_PASSWORD_RESET_START",
|
||||||
VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS",
|
VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS",
|
||||||
VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE",
|
VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE",
|
||||||
|
SET_THEME: "SET_THEME",
|
||||||
};
|
};
|
||||||
export default UserActionTypes;
|
export default UserActionTypes;
|
||||||
|
|||||||
135
util/theme.js
135
util/theme.js
@@ -1,45 +1,92 @@
|
|||||||
export default //Custom values were used as the overrides did not work.
|
const lightTheme = {
|
||||||
{
|
theme: 'light',
|
||||||
colors: {
|
colors: {
|
||||||
primary: "#1890ff",
|
primary: "#1890ff",
|
||||||
onPrimary: "#ffffff",
|
onPrimary: "#ffffff",
|
||||||
primaryContainer: "#e1e1e1ff",
|
primaryContainer: "#e1e1e1ff",
|
||||||
onPrimaryContainer: "#001c3a",
|
onPrimaryContainer: "#001c3a",
|
||||||
secondary: "#545f71",
|
secondary: "#545f71",
|
||||||
onSecondary: "#ffffff",
|
onSecondary: "#ffffff",
|
||||||
secondaryContainer: "#d8e3f8",
|
secondaryContainer: "#d8e3f8",
|
||||||
onSecondaryContainer: "#111c2b",
|
onSecondaryContainer: "#111c2b",
|
||||||
tertiary: "#00658d",
|
tertiary: "#00658d",
|
||||||
onTertiary: "#ffffff",
|
onTertiary: "#ffffff",
|
||||||
tertiaryContainer: "#c6e7ff",
|
tertiaryContainer: "#c6e7ff",
|
||||||
onTertiaryContainer: "#001e2d",
|
onTertiaryContainer: "#001e2d",
|
||||||
error: "#ba1a1a",
|
error: "#ba1a1a",
|
||||||
onError: "#ffffff",
|
onError: "#ffffff",
|
||||||
errorContainer: "#ffdad6",
|
errorContainer: "#ffdad6",
|
||||||
onErrorContainer: "#410002",
|
onErrorContainer: "#410002",
|
||||||
background: "#fdfcff",
|
background: "#fdfcff",
|
||||||
onBackground: "#1a1c1e",
|
onBackground: "#1a1c1e",
|
||||||
surface: "#fdfcff",
|
surface: "#fdfcff",
|
||||||
onSurface: "#1a1c1e",
|
onSurface: "#1a1c1e",
|
||||||
surfaceVariant: "#dededeff",
|
surfaceVariant: "#dededeff",
|
||||||
onSurfaceVariant: "#43474e",
|
onSurfaceVariant: "#43474e",
|
||||||
outline: "#74777f",
|
outline: "#74777f",
|
||||||
outlineVariant: "#c3c6cf",
|
outlineVariant: "#c3c6cf",
|
||||||
shadow: "#000000",
|
shadow: "#000000",
|
||||||
scrim: "#000000",
|
scrim: "#000000",
|
||||||
inverseSurface: "#2f3033",
|
inverseSurface: "#2f3033",
|
||||||
inverseOnSurface: "#f1f0f4",
|
inverseOnSurface: "#f1f0f4",
|
||||||
inversePrimary: "#a5c8ff",
|
inversePrimary: "#a5c8ff",
|
||||||
elevation: {
|
elevation: {
|
||||||
level0: "transparent",
|
level0: "transparent",
|
||||||
level1: "#f1f1f1ff",
|
level1: "#f1f1f1ff",
|
||||||
level2: "#e9eff9",
|
level2: "#e9eff9",
|
||||||
level3: "#e1ebf6",
|
level3: "#e1ebf6",
|
||||||
level4: "#dfe9f5",
|
level4: "#dfe9f5",
|
||||||
level5: "#dae6f4",
|
level5: "#dae6f4",
|
||||||
},
|
|
||||||
surfaceDisabled: "rgba(26, 28, 30, 0.12)",
|
|
||||||
onSurfaceDisabled: "rgba(26, 28, 30, 0.38)",
|
|
||||||
backdrop: "rgba(45, 49, 56, 0.4)",
|
|
||||||
},
|
},
|
||||||
};
|
surfaceDisabled: "rgba(26, 28, 30, 0.12)",
|
||||||
|
onSurfaceDisabled: "rgba(26, 28, 30, 0.38)",
|
||||||
|
backdrop: "rgba(45, 49, 56, 0.4)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const darkTheme = {
|
||||||
|
theme: 'dark',
|
||||||
|
colors: {
|
||||||
|
primary: "#a5c8ff",
|
||||||
|
onPrimary: "#001c3a",
|
||||||
|
primaryContainer: "#2c3e50",
|
||||||
|
onPrimaryContainer: "#d6e8ff",
|
||||||
|
secondary: "#bcc6db",
|
||||||
|
onSecondary: "#262f40",
|
||||||
|
secondaryContainer: "#3d4756",
|
||||||
|
onSecondaryContainer: "#d8e3f8",
|
||||||
|
tertiary: "#7dd3fc",
|
||||||
|
onTertiary: "#001e2d",
|
||||||
|
tertiaryContainer: "#004d66",
|
||||||
|
onTertiaryContainer: "#c6e7ff",
|
||||||
|
error: "#ffb4ab",
|
||||||
|
onError: "#690005",
|
||||||
|
errorContainer: "#93000a",
|
||||||
|
onErrorContainer: "#ffdad6",
|
||||||
|
background: "#0f1419",
|
||||||
|
onBackground: "#e4e1e6",
|
||||||
|
surface: "#0f1419",
|
||||||
|
onSurface: "#e4e1e6",
|
||||||
|
surfaceVariant: "#43474e",
|
||||||
|
onSurfaceVariant: "#c3c6cf",
|
||||||
|
outline: "#8d9199",
|
||||||
|
outlineVariant: "#43474e",
|
||||||
|
shadow: "#000000",
|
||||||
|
scrim: "#000000",
|
||||||
|
inverseSurface: "#e4e1e6",
|
||||||
|
inverseOnSurface: "#2f3033",
|
||||||
|
inversePrimary: "#1890ff",
|
||||||
|
elevation: {
|
||||||
|
level0: "transparent",
|
||||||
|
level1: "#1a1f2e",
|
||||||
|
level2: "#212837",
|
||||||
|
level3: "#293141",
|
||||||
|
level4: "#2b3344",
|
||||||
|
level5: "#2e3748",
|
||||||
|
},
|
||||||
|
surfaceDisabled: "rgba(228, 225, 230, 0.12)",
|
||||||
|
onSurfaceDisabled: "rgba(228, 225, 230, 0.38)",
|
||||||
|
backdrop: "rgba(45, 49, 56, 0.4)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export { darkTheme, lightTheme };
|
||||||
|
|||||||
Reference in New Issue
Block a user