feat: introduce high contrast mode for ios

fixes #194
This commit is contained in:
Lei Nelissen
2024-02-12 00:01:09 +01:00
parent f95c79b254
commit 82b4223939
28 changed files with 187 additions and 109 deletions

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import styled from 'styled-components/native';
import { ALPHABET_LETTERS, THEME_COLOR } from '@/CONSTANTS';
import { ALPHABET_LETTERS } from '@/CONSTANTS';
import { View, LayoutChangeEvent } from 'react-native';
import {
PanGestureHandler,
@@ -8,6 +8,7 @@ import {
TapGestureHandler,
TapGestureHandlerGestureEvent
} from 'react-native-gesture-handler';
import useDefaultStyles from './Colors';
// interface LetterContainerProps {
// onPress: (letter: string) => void;
@@ -29,7 +30,6 @@ const Letter = styled.Text`
text-align: center;
padding: 1px 0;
font-size: 12px;
color: ${THEME_COLOR};
`;
interface Props {
@@ -41,6 +41,7 @@ interface Props {
* screen with all letters of the Alphabet.
*/
const AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
const styles = useDefaultStyles();
const [ height, setHeight ] = useState(0);
const [ index, setIndex ] = useState<number>();
@@ -69,7 +70,9 @@ const AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
key={l}
onLayout={i === 0 ? handleLayout : undefined}
>
<Letter>{l}</Letter>
<Letter style={styles.themeColor}>
{l}
</Letter>
</View>
))}
</View>

View File

@@ -3,7 +3,6 @@ import { SvgProps } from 'react-native-svg';
import {
PressableProps, ViewProps, View,
} from 'react-native';
import { THEME_COLOR } from '@/CONSTANTS';
import styled, { css } from 'styled-components/native';
import useDefaultStyles from './Colors';
@@ -35,7 +34,6 @@ const BaseButton = styled.Pressable<{ size: ButtonSize }>`
`;
const ButtonText = styled.Text<{ active?: boolean, size: ButtonSize }>`
color: ${THEME_COLOR};
font-weight: 500;
font-size: 14px;
flex-shrink: 1;
@@ -72,16 +70,17 @@ const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
<Icon
width={14}
height={14}
fill={THEME_COLOR}
style={{
marginRight: title ? 8 : 0,
}}
fill={defaultStyles.themeColor.color}
style={[
{ marginRight: title ? 8 : 0 }
]}
/>
}
{title ? (
<ButtonText
active={isPressed}
size={size}
style={defaultStyles.themeColor}
numberOfLines={1}
>
{title}

View File

@@ -1,10 +1,10 @@
import { BlurView, BlurViewProps } from '@react-native-community/blur';
import { THEME_COLOR } from '@/CONSTANTS';
import React, { PropsWithChildren } from 'react';
import { useContext } from 'react';
import { ColorSchemeName, Platform, StyleSheet, View, useColorScheme } from 'react-native';
import { useTypedSelector } from '@/store';
import { ColorScheme } from '@/store/settings/types';
import { useAccessibilitySetting } from 'react-native-accessibility-settings';
const majorPlatformVersion = typeof Platform.Version === 'string' ? parseInt(Platform.Version, 10) : Platform.Version;
@@ -12,7 +12,7 @@ const majorPlatformVersion = typeof Platform.Version === 'string' ? parseInt(Pla
* Function for generating both the dark and light stylesheets, so that they
* don't have to be generate on every individual component render
*/
function generateStyles(scheme: ColorSchemeName) {
function generateStyles(scheme: ColorSchemeName, highContrast: boolean) {
return StyleSheet.create({
text: {
color: scheme === 'dark' ? '#fff' : '#000',
@@ -20,12 +20,15 @@ function generateStyles(scheme: ColorSchemeName) {
fontFamily: 'Inter',
},
textHalfOpacity: {
color: scheme === 'dark' ? '#ffffff88' : '#00000088',
color: highContrast
? (scheme === 'dark' ? '#ffffffbb' : '#000000bb')
: (scheme === 'dark' ? '#ffffff88' : '#00000088'),
fontSize: 14,
// fontFamily: 'Inter',
},
textQuarterOpacity: {
color: scheme === 'dark' ? '#ffffff44' : '#00000044',
color: highContrast
? (scheme === 'dark' ? '#ffffff88' : '#00000088')
: (scheme === 'dark' ? '#ffffff44' : '#00000044'),
fontSize: 14,
},
view: {
@@ -35,7 +38,9 @@ function generateStyles(scheme: ColorSchemeName) {
borderColor: scheme === 'dark' ? '#262626' : '#ddd',
},
activeBackground: {
backgroundColor: `${THEME_COLOR}${scheme === 'dark' ? '26' : '16'}`,
backgroundColor: highContrast
? `#8b513c${scheme === 'dark' ? '26' : '10'}`
: `#FF3C00${scheme === 'dark' ? '26' : '16'}`,
},
imageBackground: {
backgroundColor: scheme === 'dark' ? '#191919' : '#eee',
@@ -49,7 +54,9 @@ function generateStyles(scheme: ColorSchemeName) {
backgroundColor: scheme === 'dark' ? '#000' : '#fff',
},
button: {
backgroundColor: scheme === 'dark' ? '#ffffff09' : '#00000009',
backgroundColor: highContrast
? (scheme === 'dark' ? '#ffffff0f' : '#0000000f')
: (scheme === 'dark' ? '#ffffff09' : '#00000009'),
},
input: {
backgroundColor: scheme === 'dark' ? '#191919' : '#f3f3f3',
@@ -67,13 +74,35 @@ function generateStyles(scheme: ColorSchemeName) {
filter: {
backgroundColor: scheme === 'dark' ? '#191919' : '#f3f3f3',
},
themeColor: {
color: highContrast
? scheme === 'dark' ? '#FF7A1C' : '#c93400'
: '#FF3C00',
},
themeColorHalfOpacity: {
color: highContrast
? scheme === 'dark' ? '#FF7A1Cbb' : '#c93400bb'
: '#FF3C0088',
},
themeColorQuarterOpacity: {
color: highContrast
? scheme === 'dark' ? '#FF7A1C88' : '#c9340088'
: '#FF3C0044',
},
themeBackground: {
backgroundColor: highContrast
? scheme === 'dark' ? '#FF7A1C' : '#c93400'
: '#FF3C00',
}
});
}
// Prerender both stylesheets
export const themes: Record<'dark' | 'light', ReturnType<typeof generateStyles>> = {
'dark': generateStyles('dark'),
'light': generateStyles('light'),
export const themes: Record<'dark' | 'light' | 'dark-highcontrast' | 'light-highcontrast', ReturnType<typeof generateStyles>> = {
'dark': generateStyles('dark', false),
'light': generateStyles('light', false),
'dark-highcontrast': generateStyles('dark', true),
'light-highcontrast': generateStyles('light', true),
};
// Create context for supplying the theming information
@@ -84,9 +113,12 @@ export const ColorSchemeContext = React.createContext(themes.dark);
*/
export function ColorSchemeProvider({ children }: PropsWithChildren<{}>) {
const systemScheme = useColorScheme();
const highContrast = useAccessibilitySetting('darkerSystemColors');
const userScheme = useTypedSelector((state) => state.settings.colorScheme);
const scheme = userScheme === ColorScheme.System ? systemScheme : userScheme;
const theme = themes[scheme || 'light'];
const theme = highContrast
? themes[`${scheme || 'light'}-highcontrast`]
: themes[scheme || 'light'];
return (
<ColorSchemeContext.Provider value={theme}>

View File

@@ -6,13 +6,14 @@ import CloudExclamationMarkIcon from '@/assets/icons/cloud-exclamation-mark.svg'
import InternalDriveIcon from '@/assets/icons/internal-drive.svg';
import useDefaultStyles from './Colors';
import Svg, { Circle, CircleProps } from 'react-native-svg';
import { Animated, Easing } from 'react-native';
import { Animated, Easing, ViewProps } from 'react-native';
import styled from 'styled-components/native';
interface DownloadIconProps {
trackId: string;
size?: number;
fill?: string;
style?: ViewProps['style'];
}
const DownloadContainer = styled.View`
@@ -26,7 +27,7 @@ const IconOverlay = styled.View`
transform: scale(0.5);
`;
function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
function DownloadIcon({ trackId, size = 16, fill, style }: DownloadIconProps) {
// determine styles
const defaultStyles = useDefaultStyles();
const iconFill = fill || defaultStyles.textQuarterOpacity.color;
@@ -66,19 +67,19 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
if (!entity && !isQueued) {
return (
<CloudIcon width={size} height={size} fill={iconFill} />
<CloudIcon width={size} height={size} fill={iconFill} style={style} />
);
}
if (entity?.isComplete) {
return (
<InternalDriveIcon width={size} height={size} fill={iconFill} />
<InternalDriveIcon width={size} height={size} fill={iconFill} style={style} />
);
}
if (entity?.isFailed) {
return (
<CloudExclamationMarkIcon width={size} height={size} fill={iconFill} />
<CloudExclamationMarkIcon width={size} height={size} fill={iconFill} style={style} />
);
}
@@ -100,7 +101,7 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
/>
</Svg>
<IconOverlay>
<CloudDownArrow width={size} height={size} fill={iconFill} />
<CloudDownArrow width={size} height={size} fill={iconFill} style={style} />
</IconOverlay>
</DownloadContainer>
);

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import { TouchableOpacityProps } from 'react-native';
import ChevronRight from '@/assets/icons/chevron-right.svg';
import styled from 'styled-components/native';
import { THEME_COLOR } from '@/CONSTANTS';
import useDefaultStyles from './Colors';
const BUTTON_SIZE = 14;
@@ -17,7 +16,6 @@ const Container = styled.Pressable<{ active?: boolean }>`
`;
const Label = styled.Text<{ active?: boolean }>`
color: ${THEME_COLOR};
font-size: 16px;
`;
@@ -37,8 +35,14 @@ const ListButton: React.FC<TouchableOpacityProps> = ({ children, ...props }) =>
isPressed ? defaultStyles.activeBackground : undefined
]}
>
<Label>{children}</Label>
<ChevronRight width={BUTTON_SIZE} height={BUTTON_SIZE} fill={THEME_COLOR} />
<Label style={defaultStyles.themeColor}>
{children}
</Label>
<ChevronRight
width={BUTTON_SIZE}
height={BUTTON_SIZE}
fill={defaultStyles.themeColor.color}
/>
</Container>
);
};

View File

@@ -1,4 +1,3 @@
import { THEME_COLOR } from '@/CONSTANTS';
import styled from 'styled-components/native';
import Animated from 'react-native-reanimated';
@@ -51,7 +50,6 @@ const ProgressTrack = styled(Animated.View)<ProgressTrackProps>`
left: 0;
right: 0;
height: ${(props) => props.stroke ? props.stroke + 'px' : '100%'};
background-color: ${THEME_COLOR};
opacity: ${(props) => props.opacity || 1};
border-radius: 99px;
`;