Skip to content

Commit

Permalink
add icon/ theme switch
Browse files Browse the repository at this point in the history
  • Loading branch information
lukesthl committed Dec 20, 2023
1 parent 611ae69 commit dbdaefa
Show file tree
Hide file tree
Showing 22 changed files with 508 additions and 217 deletions.
19 changes: 18 additions & 1 deletion apps/expo/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
slug: "digitalbreak",
version: "1.0.0",
orientation: "portrait",
icon: "./assets/images/icon.png",
icon: "./assets/images/default.png",
scheme: "myapp",
userInterfaceStyle: "automatic",
splash: {
Expand All @@ -32,6 +32,23 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
plugins: [
"expo-router",
[
"expo-dynamic-app-icon",
{
default: {
image: "./assets/images/default.png",
prerendered: true,
},
light: {
image: "./assets/images/light.png",
prerendered: true,
},
dark: {
image: "./assets/images/dark.png",
prerendered: true,
},
},
],
[
"expo-build-properties",
{
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/app/(tabs)/overview/[appId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const App = observer(() => {
}
%
</SizableText>{" "}
of your attempts were on this app.
of your attempts were on {selectedApp.name}.
</Paragraph>
<View overflow="hidden" flexDirection="row" justifyContent="center">
<PieChart
Expand Down
87 changes: 41 additions & 46 deletions apps/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useRef, useState } from "react";
import { AppState, useColorScheme } from "react-native";
import { useEffect, useRef } from "react";
import { AppState } from "react-native";
import { useFonts } from "expo-font";
import { router, SplashScreen, Stack } from "expo-router";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { getTokenValue, TamaguiProvider, Theme } from "tamagui";
import { DarkTheme, DefaultTheme, ThemeProvider as NavigationThemeProvider } from "@react-navigation/native";
import { getTokenValue, TamaguiProvider } from "tamagui";

import "../data/logger";

import { ThemeProvider, useTheme } from "../components/theme-provider";
import { clearShortcutListener, listenForShortcut } from "../data/shortcut.listener";
import config from "../tamagui.config";

Expand Down Expand Up @@ -36,20 +37,7 @@ export default function RootLayout() {
}

function RootLayoutNav() {
const colorScheme = useColorScheme();
const [currentColorScheme, setCurrentColorScheme] = useState(colorScheme);
const onColorSchemeChange = useRef<NodeJS.Timeout>();

const appState = useRef(AppState.currentState);

useEffect(() => {
if (colorScheme !== currentColorScheme) {
onColorSchemeChange.current = setTimeout(() => setCurrentColorScheme(colorScheme), 1000);
} else if (onColorSchemeChange.current) {
clearTimeout(onColorSchemeChange.current);
}
}, [colorScheme, currentColorScheme]);

useEffect(() => {
const checkShortcut = () => {
void listenForShortcut()
Expand Down Expand Up @@ -82,35 +70,42 @@ function RootLayoutNav() {
}, []);
return (
<TamaguiProvider config={config}>
<Theme name={currentColorScheme === "dark" ? "dark" : "light"}>
<ThemeProvider
value={
currentColorScheme === "light"
? {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: "#FFFFFF",
text: getTokenValue("$text11") as string,
primary: getTokenValue("$text11") as string,
border: getTokenValue("$grey3") as string,
},
}
: {
...DarkTheme,
colors: {
...DarkTheme.colors,
},
}
}
>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="break" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false, presentation: "modal" }} />
</Stack>
</ThemeProvider>
</Theme>
<ThemeProvider>
<NavigationStack />
</ThemeProvider>
</TamaguiProvider>
);
}

const NavigationStack = () => {
const { theme } = useTheme();
return (
<NavigationThemeProvider
value={
theme === "light"
? {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: "#FFFFFF",
text: getTokenValue("$text11") as string,
primary: getTokenValue("$text11") as string,
border: getTokenValue("$grey3") as string,
},
}
: {
...DarkTheme,
colors: {
...DarkTheme.colors,
},
}
}
>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="break" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false, presentation: "modal" }} />
</Stack>
</NavigationThemeProvider>
);
};
12 changes: 12 additions & 0 deletions apps/expo/app/settings/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ const SettingsLayout = observer(() => {
},
}}
/>
<Stack.Screen
name="app-icon"
options={{
title: "App Icon",
}}
/>
<Stack.Screen
name="theme"
options={{
title: "Theme",
}}
/>
</Stack>
);
});
Expand Down
69 changes: 69 additions & 0 deletions apps/expo/app/settings/app-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { useState } from "react";
import type { ImageURISource } from "react-native";
import { getAppIcon, setAppIcon } from "expo-dynamic-app-icon";
import { Check } from "@tamagui/lucide-icons";
import { Image, ListItem, View, YGroup, YStack } from "tamagui";

import { Container } from "../../components/container";

const DarkIcon = require("../../assets/images/default.png") as ImageURISource;
const DefaultIcon = require("../../assets/images/default.png") as ImageURISource;
const LightIcon = require("../../assets/images/light.png") as ImageURISource;

const icons = [
{
name: "Default",
value: "default",
source: DefaultIcon,
},
{
name: "Light",
value: "light",
source: LightIcon,
},
{
name: "Dark",
value: "dark",
source: DarkIcon,
},
] as const;

const AppIcon = () => {
const [activeIcon, setActiveIcon] = useState(getAppIcon().toLowerCase());

const setIcon = (value: (typeof icons)[number]["value"]) => {
setActiveIcon(value);
setAppIcon(value);
};

return (
<Container paddingVertical={"$4"}>
<YStack space="$3">
<YGroup alignSelf="center" bordered size="$4">
{icons.map((icon) => (
<YGroup.Item key={icon.value}>
<ListItem
hoverTheme
pressTheme
icon={
<View backgroundColor="$gray5" borderRadius={"$3"} padding="$2">
<Image source={icon.source} style={{ width: 36, height: 36 }} />
</View>
}
onPress={() => {
void setIcon(icon.value);
}}
iconAfter={activeIcon === icon.value ? <Check size={18} strokeWidth={2.5} /> : undefined}
>
<ListItem.Text>{icon.name}</ListItem.Text>
</ListItem>
</YGroup.Item>
))}
</YGroup>
</YStack>
</Container>
);
};

export default AppIcon;
6 changes: 6 additions & 0 deletions apps/expo/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const Settings = () => (
<Box size={20} />
</View>
}
onPress={() => {
router.push("/settings/app-icon");
}}
iconAfter={ChevronRight}
>
<ListItem.Text>{"App Icon"}</ListItem.Text>
Expand All @@ -52,6 +55,9 @@ const Settings = () => (
<SunMoon size={20} />
</View>
}
onPress={() => {
router.push("/settings/theme");
}}
iconAfter={ChevronRight}
>
<ListItem.Text>{"Theme"}</ListItem.Text>
Expand Down
52 changes: 52 additions & 0 deletions apps/expo/app/settings/theme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Check, Moon, Smartphone, Sun } from "@tamagui/lucide-icons";
import { ListItem, View, YGroup, YStack } from "tamagui";

import { Container } from "../../components/container";
import { useTheme } from "../../components/theme-provider";
import { themeTypes } from "../../theme/theme-builder";

const Theme = () => {
const { theme, setTheme, autoTheme } = useTheme();
return (
<Container paddingVertical={"$4"}>
<YStack space="$3">
<YGroup alignSelf="center" bordered size="$4">
{(["auto", ...themeTypes] as const).map((themeType) => (
<YGroup.Item key={themeType}>
<ListItem
hoverTheme
pressTheme
icon={
<View backgroundColor="$gray5" borderRadius={"$3"} padding="$2">
{themeType === "light" ? (
<Sun size={24} />
) : themeType === "auto" ? (
<Smartphone size={24} />
) : (
<Moon size={24} />
)}
</View>
}
onPress={() => {
setTheme(themeType);
}}
iconAfter={
(autoTheme && themeType === "auto") || (theme === themeType && !autoTheme) ? (
<Check size={18} strokeWidth={2.5} />
) : undefined
}
>
<ListItem.Text>
{themeType === "light" ? "Light" : themeType === "auto" ? "Auto" : "Dark"}
</ListItem.Text>
</ListItem>
</YGroup.Item>
))}
</YGroup>
</YStack>
</Container>
);
};

export default Theme;
Binary file added apps/expo/assets/images/dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added apps/expo/assets/images/light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions apps/expo/components/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createContext, useContext, useEffect, useRef, useState } from "react";
import { useColorScheme } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Theme } from "tamagui";

export const ThemeContext = createContext<{
theme: ThemeType;
setTheme: (theme: ThemeType | "auto") => void;
autoTheme: boolean;
}>({
theme: "light",
setTheme: () => {
console.warn("Missing ThemeProvider");
},
autoTheme: false,
});

export type ThemeType = "light" | "dark";

export const useTheme = () => useContext(ThemeContext);

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setThemeState] = useState<ThemeType>("light");
const [autoTheme, setAutoTheme] = useState<boolean>(false);

const colorScheme = useColorScheme();
const [currentColorScheme, setCurrentColorScheme] = useState(colorScheme);
const onColorSchemeChange = useRef<NodeJS.Timeout>();

useEffect(() => {
const init = async () => {
if (colorScheme !== currentColorScheme) {
onColorSchemeChange.current = setTimeout(() => setCurrentColorScheme(colorScheme), 1000);
} else if (onColorSchemeChange.current) {
clearTimeout(onColorSchemeChange.current);
}
const storedTheme = await AsyncStorage.getItem("theme");
setThemeState(storedTheme as ThemeType);
if (storedTheme === "auto") {
setThemeState(colorScheme === "dark" ? "dark" : "light");
}
};
void init();
}, [colorScheme, currentColorScheme]);

const setTheme = (theme: ThemeType | "auto") => {
if (theme === "auto") {
setThemeState(colorScheme === "dark" ? "dark" : "light");
} else {
setThemeState(theme);
}
setAutoTheme(theme === "auto");
void AsyncStorage.setItem("theme", theme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, autoTheme }}>
<Theme name={theme}>{children}</Theme>
</ThemeContext.Provider>
);
};
Loading

0 comments on commit dbdaefa

Please sign in to comment.