Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {CONFIGCAT_SDK_KEY_TEST, CONFIGCAT_SDK_KEY_PROD} from '@env';
import {BannerProvider} from './src/Components/UIComp/AnimeAdBanner/BannerContext';
import crashlytics from '@react-native-firebase/crashlytics';
import analytics from '@react-native-firebase/analytics';
import {ThemeProvider} from './src/Theme';

/**
* The main App component that sets up the root of the application.
Expand Down Expand Up @@ -54,13 +55,15 @@ const App = () => {
sdkKey={__DEV__ ? CONFIGCAT_SDK_KEY_TEST : CONFIGCAT_SDK_KEY_PROD}>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<PaperProvider>
<BannerProvider>
<RootNavigation />
<Toast />
<ForceUpdate />
</BannerProvider>
</PaperProvider>
<ThemeProvider>
<PaperProvider>
<BannerProvider>
<RootNavigation />
<Toast />
<ForceUpdate />
</BannerProvider>
</PaperProvider>
</ThemeProvider>
</PersistGate>
</Provider>
</ConfigCatProvider>
Expand Down
8 changes: 5 additions & 3 deletions src/Navigation/BottomNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {View, StyleSheet} from 'react-native';
import {useFeatureFlag} from 'configcat-react';
import LinkListScreen from '../InkNest-Externals/Screens/Webview/LinkListScreen';
import FloatingDonationButton from '../InkNest-Externals/Donation/FloatingDonationButton';
import {useTheme} from '../Theme';

const BottomTab = createBottomTabNavigator();

Expand Down Expand Up @@ -78,6 +79,7 @@ const TabBarIcon = props => {
*/
export function BottomNavigation() {
const {value: forIosValue} = useFeatureFlag('forIos', 'Default');
const {colors} = useTheme();

return (
<>
Expand All @@ -88,14 +90,14 @@ export function BottomNavigation() {
style={[
StyleSheet.absoluteFill,
{
backgroundColor: '#110918',
backgroundColor: colors.tabBar,
},
]}
/>
),
headerShown: false,
tabBarActiveTintColor: '#D2D2D6',
tabBarInactiveTintColor: '#6B666D',
tabBarActiveTintColor: colors.tabBarActive,
tabBarInactiveTintColor: colors.tabBarInactive,
tabBarStyle: {
paddingVertical: 4,
},
Expand Down
6 changes: 6 additions & 0 deletions src/Redux/Reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const initialState = {
DownloadComic: {},
scrollPreference: 'horizontal', // Default scroll mode is horizontal
hasRewardAdsShown: false,
themeMode: 'system', // Theme mode: 'light', 'dark', or 'system'
};

/**
Expand Down Expand Up @@ -200,6 +201,10 @@ const Reducers = createSlice({
// Update the flag indicating whether reward ads have been shown
state.hasRewardAdsShown = action.payload;
},
setThemeMode: (state, action) => {
// Update the theme mode: 'light', 'dark', or 'system'
state.themeMode = action.payload;
},
},
});

Expand All @@ -224,5 +229,6 @@ export const {
clearHistory,
setScrollPreference,
rewardAdsShown,
setThemeMode,
} = Reducers.actions;
export default Reducers.reducer;
127 changes: 120 additions & 7 deletions src/Screens/Settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,136 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {NAVIGATION} from '../../Constants';
import Header from '../../Components/UIComp/Header';
import {useDispatch, useSelector} from 'react-redux';
import {setScrollPreference} from '../../Redux/Reducers';
import {setScrollPreference, setThemeMode} from '../../Redux/Reducers';
import DonationBanner from '../../InkNest-Externals/Donation/DonationBanner';
import {useTheme} from '../../Theme';

/**
* Get the display label for the current theme mode
* @param {string} mode - The theme mode ('light', 'dark', or 'system')
* @returns {string} The display label
*/
const getThemeModeLabel = mode => {
switch (mode) {
case 'light':
return 'Light';
case 'dark':
return 'Dark';
case 'system':
default:
return 'System';
}
};

/**
* Get the icon name for the current theme mode
* @param {string} mode - The theme mode ('light', 'dark', or 'system')
* @returns {string} The icon name
*/
const getThemeModeIcon = mode => {
switch (mode) {
case 'light':
return 'white-balance-sunny';
case 'dark':
return 'moon-waning-crescent';
case 'system':
default:
return 'theme-light-dark';
}
};

export function Settings({navigation}) {
const dispatch = useDispatch();
const scrollPreference = useSelector(state => state.data.scrollPreference);
const themeMode = useSelector(state => state.data.themeMode) || 'system';
const {colors} = useTheme();

/**
* Cycle through theme modes: system -> light -> dark -> system
*/
const handleThemeToggle = () => {
analytics().logEvent('toggle_theme_mode', {
item: 'Theme Mode',
currentMode: themeMode,
});

let newMode;
switch (themeMode) {
case 'system':
newMode = 'light';
break;
case 'light':
newMode = 'dark';
break;
case 'dark':
default:
newMode = 'system';
break;
}

dispatch(setThemeMode(newMode));

Alert.alert(
'Theme Changed',
`App theme is now set to ${getThemeModeLabel(newMode)}.`,
[{text: 'OK'}],
);
};

return (
<SafeAreaView style={{flex: 1, backgroundColor: '#14142A'}} edges={['top']}>
<Header title="Settings" />
<SafeAreaView
style={{flex: 1, backgroundColor: colors.background}}
edges={['top']}>
<Header
title="Settings"
style={{backgroundColor: colors.headerBackground}}
TitleStyle={{color: colors.text}}
/>
<DonationBanner />

<ScrollView showsVerticalScrollIndicator={false}>
<TouchableOpacity
style={{
paddingVertical: hp('1%'),
backgroundColor: '#FFF',
backgroundColor: colors.settingsItem,
marginHorizontal: widthPercentageToDP('2%'),
marginVertical: hp('1%'),
paddingHorizontal: 10,
borderRadius: 5,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
onPress={handleThemeToggle}>
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The theme toggle TouchableOpacity is missing accessibility properties. Consider adding accessibilityRole="button", accessibilityLabel, and accessibilityHint to improve screen reader support. For example:

accessibilityRole="button"
accessibilityLabel={`Theme: ${getThemeModeLabel(themeMode)}`}
accessibilityHint="Double tap to cycle through System, Light, and Dark theme modes"
Suggested change
onPress={handleThemeToggle}>
onPress={handleThemeToggle}
accessibilityRole="button"
accessibilityLabel={`Theme: ${getThemeModeLabel(themeMode)}`}
accessibilityHint="Double tap to cycle through System, Light, and Dark theme modes"
>

Copilot uses AI. Check for mistakes.
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<MaterialCommunityIcons
name={getThemeModeIcon(themeMode)}
size={hp('2.5%')}
color={colors.settingsItemText}
style={{marginRight: 10}}
/>
<Text
style={{
fontSize: hp('2%'),
fontWeight: 'bold',
color: colors.settingsItemText,
}}>
Theme
</Text>
</View>
<Text
style={{
fontSize: hp('1.8%'),
color: colors.link,
}}>
{getThemeModeLabel(themeMode)}
</Text>
</TouchableOpacity>

<TouchableOpacity
style={{
paddingVertical: hp('1%'),
backgroundColor: colors.settingsItem,
marginHorizontal: widthPercentageToDP('2%'),
marginVertical: hp('1%'),
paddingHorizontal: 10,
Expand All @@ -60,14 +173,14 @@ export function Settings({navigation}) {
<Ionicons
name="information-circle-outline"
size={hp('2.5%')}
color="#000"
color={colors.settingsItemText}
style={{marginRight: 10}}
/>
<Text
style={{
fontSize: hp('2%'),
fontWeight: 'bold',
color: '#000',
color: colors.settingsItemText,
}}>
About us
</Text>
Expand Down Expand Up @@ -231,7 +344,7 @@ export function Settings({navigation}) {
<Text
style={{
fontSize: hp('1.8%'),
color: '#007AFF',
color: colors.link,
}}>
{scrollPreference === 'horizontal' ? 'Horizontal' : 'Vertical'}
</Text>
Expand Down
76 changes: 76 additions & 0 deletions src/Theme/ThemeContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, {createContext, useContext, useMemo} from 'react';
import {useSelector} from 'react-redux';
import {useColorScheme} from 'react-native';
import {lightColors, darkColors} from './colors';

/**
* @typedef {'light' | 'dark' | 'system'} ThemeMode
*/

/**
* Theme context that provides the current theme colors and mode information.
*/
const ThemeContext = createContext({
colors: darkColors,
isDark: true,
themeMode: 'system',
});

/**
* ThemeProvider component that wraps the application and provides theme context.
* It reads the theme preference from Redux store and the system color scheme
* to determine the current theme.
*
* @param {Object} props - Component props
* @param {React.ReactNode} props.children - Child components
* @returns {JSX.Element} The provider component wrapping children
*/
export function ThemeProvider({children}) {
const systemColorScheme = useColorScheme();
const themeMode = useSelector(state => state.data.themeMode) || 'system';

const {colors, isDark} = useMemo(() => {
let isDarkTheme;

if (themeMode === 'system') {
isDarkTheme = systemColorScheme === 'dark';
} else {
isDarkTheme = themeMode === 'dark';
}

return {
colors: isDarkTheme ? darkColors : lightColors,
isDark: isDarkTheme,
};
}, [themeMode, systemColorScheme]);

const value = useMemo(
() => ({
colors,
isDark,
themeMode,
}),
[colors, isDark, themeMode],
);

return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}

/**
* Custom hook to access the current theme context.
* Must be used within a ThemeProvider.
*
* @returns {{ colors: typeof darkColors, isDark: boolean, themeMode: string }}
* The current theme colors, dark mode flag, and theme mode setting
*/
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check if (!context) will never be true because the ThemeContext is created with a default value (lines 13-17). This error handling won't work as intended. Consider removing the default value from createContext() (pass undefined instead) to make this check effective, or remove the check entirely if a default fallback is desired.

Copilot uses AI. Check for mistakes.
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

export {lightColors, darkColors};
Loading
Loading