Theme done

This commit is contained in:
Zhiyuan Zheng 2020-11-29 18:08:31 +01:00
parent 24d0681c9e
commit 1493e20962
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
24 changed files with 280 additions and 150 deletions

35
App.tsx
View File

@ -28,22 +28,25 @@ setConsole({
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
<AppearanceProvider> <AppearanceProvider>
<ThemeManager> <ReactQueryCacheProvider queryCache={queryCache}>
<ReactQueryCacheProvider queryCache={queryCache}> <Provider store={store}>
<Provider store={store}> <PersistGate persistor={persistor}>
<PersistGate persistor={persistor}> {bootstrapped => {
{bootstrapped => { if (bootstrapped) {
if (bootstrapped) { console.log('Bootstrapped!')
require('src/i18n/i18n') require('src/i18n/i18n')
return <Index /> return (
} else { <ThemeManager>
return <></> <Index />
} </ThemeManager>
}} )
</PersistGate> } else {
</Provider> return <></>
</ReactQueryCacheProvider> }
</ThemeManager> }}
</PersistGate>
</Provider>
</ReactQueryCacheProvider>
</AppearanceProvider> </AppearanceProvider>
) )
} }

View File

@ -4,6 +4,7 @@ import { NavigationContainer } from '@react-navigation/native'
import { enableScreens } from 'react-native-screens' import { enableScreens } from 'react-native-screens'
import React from 'react' import React from 'react'
import { StatusBar } from 'react-native'
import Toast from 'react-native-toast-message' import Toast from 'react-native-toast-message'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
@ -30,65 +31,72 @@ export type RootStackParamList = {
export const Index: React.FC = () => { export const Index: React.FC = () => {
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
enum barStyle {
light = 'dark-content',
dark = 'light-content'
}
return ( return (
<NavigationContainer theme={themes[mode]}> <>
<Tab.Navigator <StatusBar barStyle={barStyle[mode]} />
screenOptions={({ route }) => ({ <NavigationContainer theme={themes[mode]}>
tabBarIcon: ({ focused, color, size }) => { <Tab.Navigator
let name: string screenOptions={({ route }) => ({
switch (route.name) { tabBarIcon: ({ focused, color, size }) => {
case 'Screen-Local': let name: string
name = 'home' switch (route.name) {
break case 'Screen-Local':
case 'Screen-Public': name = 'home'
name = 'globe' break
break case 'Screen-Public':
case 'Screen-Post': name = 'globe'
name = 'plus' break
break case 'Screen-Post':
case 'Screen-Notifications': name = 'plus'
name = 'bell' break
break case 'Screen-Notifications':
case 'Screen-Me': name = 'bell'
name = focused ? 'smile' : 'meh' break
break case 'Screen-Me':
default: name = focused ? 'smile' : 'meh'
name = 'alert-octagon' break
break default:
} name = 'alert-octagon'
return <Feather name={name} size={size} color={color} /> break
} }
})} return <Feather name={name} size={size} color={color} />
tabBarOptions={{
activeTintColor: theme.primary,
inactiveTintColor: theme.secondary,
showLabel: false
}}
>
<Tab.Screen name='Screen-Local' component={ScreenLocal} />
<Tab.Screen name='Screen-Public' component={ScreenPublic} />
<Tab.Screen
name='Screen-Post'
listeners={({ navigation, route }) => ({
tabPress: e => {
e.preventDefault()
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose'
})
} }
})} })}
tabBarOptions={{
activeTintColor: theme.primary,
inactiveTintColor: theme.secondary,
showLabel: false
}}
> >
{() => <></>} <Tab.Screen name='Screen-Local' component={ScreenLocal} />
</Tab.Screen> <Tab.Screen name='Screen-Public' component={ScreenPublic} />
<Tab.Screen <Tab.Screen
name='Screen-Notifications' name='Screen-Post'
component={ScreenNotifications} listeners={({ navigation, route }) => ({
/> tabPress: e => {
<Tab.Screen name='Screen-Me' component={ScreenMe} /> e.preventDefault()
</Tab.Navigator> navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose'
})
}
})}
>
{() => <></>}
</Tab.Screen>
<Tab.Screen
name='Screen-Notifications'
component={ScreenNotifications}
/>
<Tab.Screen name='Screen-Me' component={ScreenMe} />
</Tab.Navigator>
<Toast ref={(ref: any) => Toast.setRef(ref)} config={toastConfig} /> <Toast ref={(ref: any) => Toast.setRef(ref)} config={toastConfig} />
</NavigationContainer> </NavigationContainer>
</>
) )
} }

View File

@ -44,7 +44,7 @@ export interface Props {
const Timelines: React.FC<Props> = ({ name, content }) => { const Timelines: React.FC<Props> = ({ name, content }) => {
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme() const { mode, theme } = useTheme()
const localRegistered = useSelector(getLocalUrl) const localRegistered = useSelector(getLocalUrl)
const publicDomain = useSelector(getRemoteUrl) const publicDomain = useSelector(getRemoteUrl)
const [segment, setSegment] = useState(0) const [segment, setSegment] = useState(0)
@ -107,6 +107,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
headerCenter: () => ( headerCenter: () => (
<View style={styles.segmentsContainer}> <View style={styles.segmentsContainer}>
<SegmentedControl <SegmentedControl
appearance={mode}
values={[content[0].title, content[1].title]} values={[content[0].title, content[1].title]}
selectedIndex={segment} selectedIndex={segment}
onChange={onChangeSegment} onChange={onChangeSegment}

View File

@ -88,6 +88,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
emojis={actualStatus.account.emojis} emojis={actualStatus.account.emojis}
account={actualStatus.account.acct} account={actualStatus.account.acct}
created_at={item.created_at} created_at={item.created_at}
visibility={item.visibility}
application={item.application} application={item.application}
/> />
{/* Can pass toot info to next page to speed up performance */} {/* Can pass toot info to next page to speed up performance */}

View File

@ -98,6 +98,7 @@ export interface Props {
emojis?: Mastodon.Emoji[] emojis?: Mastodon.Emoji[]
account: string account: string
created_at: string created_at: string
visibility: Mastodon.Status['visibility']
application?: Mastodon.Application application?: Mastodon.Application
} }
@ -109,6 +110,7 @@ const HeaderDefault: React.FC<Props> = ({
emojis, emojis,
account, account,
created_at, created_at,
visibility,
application application
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
@ -197,6 +199,14 @@ const HeaderDefault: React.FC<Props> = ({
{since} {since}
</Text> </Text>
</View> </View>
{visibility === 'private' && (
<Feather
name='lock'
size={constants.FONT_SIZE_S}
color={theme.secondary}
style={styles.visibility}
/>
)}
{application && application.name !== 'Web' && ( {application && application.name !== 'Web' && (
<View> <View>
<Text <Text
@ -292,12 +302,16 @@ const styles = StyleSheet.create({
}, },
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
marginTop: constants.SPACING_XS, marginTop: constants.SPACING_XS,
marginBottom: constants.SPACING_S marginBottom: constants.SPACING_S
}, },
created_at: { created_at: {
fontSize: constants.FONT_SIZE_S fontSize: constants.FONT_SIZE_S
}, },
visibility: {
marginLeft: constants.SPACING_S
},
application: { application: {
fontSize: constants.FONT_SIZE_S, fontSize: constants.FONT_SIZE_S,
marginLeft: constants.SPACING_S marginLeft: constants.SPACING_S

View File

@ -9,9 +9,8 @@ import {
getSettingsLanguage getSettingsLanguage
} from 'src/utils/slices/settingsSlice' } from 'src/utils/slices/settingsSlice'
import { store } from 'src/store' import { store } from 'src/store'
console.log(store.getState())
if (!getSettingsLanguage(store.getState())) { if (!getSettingsLanguage(store.getState())) {
console.log('No default locale of app')
const deviceLocal = Localization.locale const deviceLocal = Localization.locale
if (deviceLocal.startsWith('zh')) { if (deviceLocal.startsWith('zh')) {
store.dispatch(changeLanguage('zh')) store.dispatch(changeLanguage('zh'))

View File

@ -1,4 +1,11 @@
export default { export default {
common: require('./common').default, common: require('./common').default,
settings: require('./settings').default
meRoot: require('./screens/meRoot').default,
meConversations: require('./screens/meConversations').default,
meBookmarks: require('./screens/meBookmarks').default,
meFavourites: require('./screens/meFavourites').default,
meLists: require('./screens/meLists').default,
meListsList: require('./screens/meListsList').default,
meSettings: require('./screens/meSettings').default
} }

View File

@ -1,4 +1,7 @@
export default { export default {
buttons: {
cancel: '取消'
},
headers: { headers: {
local: { local: {
segments: { segments: {
@ -12,20 +15,6 @@ export default {
right: '外站嘟嘟' right: '外站嘟嘟'
} }
}, },
notifications: '我的通知', notifications: '我的通知'
me: {
root: '我的长毛象',
conversations: '私信',
bookmarks: '书签',
favourites: '喜欢',
lists: {
root: '列表',
list: '列表 {{list}}'
},
settings: {
root: '设置',
language: '语言'
}
}
} }
} }

View File

@ -0,0 +1,4 @@
export default {
heading: '书签',
content: {}
}

View File

@ -0,0 +1,4 @@
export default {
heading: '私信',
content: {}
}

View File

@ -0,0 +1,4 @@
export default {
heading: '收藏',
content: {}
}

View File

@ -0,0 +1,4 @@
export default {
heading: '列表',
content: {}
}

View File

@ -0,0 +1,4 @@
export default {
heading: '列表 {{list}}',
content: {}
}

View File

@ -0,0 +1,24 @@
export default {
heading: '我的长毛象',
content: {
login: {},
collections: {
conversations: '$t(meConversations:heading)',
bookmarks: '$t(meBookmarks:heading)',
favourites: '$t(meFavourites:heading)',
lists: '$t(meLists:heading)'
},
settings: '$t(meSettings:heading)',
logout: {
button: '退出当前账号',
alert: {
title: '确认退出登录?',
message: '退出登录后,需要重新认证账号',
buttons: {
logout: '退出登录',
cancel: '$t(common:buttons.cancel)'
}
}
}
}
}

View File

@ -0,0 +1,22 @@
export default {
heading: '设置',
content: {
language: {
heading: '切换语言',
options: {
zh: '简体中文',
en: 'English',
cancel: '$t(common:buttons.cancel)'
}
},
theme: {
heading: '颜色模式',
options: {
auto: '跟随系统',
light: '浅色模式',
dark: '深色模式',
cancel: '$t(common:buttons.cancel)'
}
}
}
}

View File

@ -1,11 +0,0 @@
export default {
content: {
language: {
title: '切换语言',
options: {
zh: '简体中文',
en: 'English'
}
}
}
}

View File

@ -41,42 +41,42 @@ const ScreenMe: React.FC = () => {
name='Screen-Me-Conversations' name='Screen-Me-Conversations'
component={ScreenMeConversations} component={ScreenMeConversations}
options={{ options={{
headerTitle: t('headers.me.conversations') headerTitle: t('meConversations:heading')
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Me-Bookmarks' name='Screen-Me-Bookmarks'
component={ScreenMeBookmarks} component={ScreenMeBookmarks}
options={{ options={{
headerTitle: t('headers.me.bookmarks') headerTitle: t('meBookmarks:heading')
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Me-Favourites' name='Screen-Me-Favourites'
component={ScreenMeFavourites} component={ScreenMeFavourites}
options={{ options={{
headerTitle: t('headers.me.favourites') headerTitle: t('meFavourites:heading')
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Me-Lists' name='Screen-Me-Lists'
component={ScreenMeLists} component={ScreenMeLists}
options={{ options={{
headerTitle: t('headers.me.lists.root') headerTitle: t('meLists:heading')
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Me-Lists-List' name='Screen-Me-Lists-List'
component={ScreenMeListsList} component={ScreenMeListsList}
options={({ route }: any) => ({ options={({ route }: any) => ({
headerTitle: t('headers.me.lists.list', { list: route.params.title }) headerTitle: t('meListsList:heading', { list: route.params.title })
})} })}
/> />
<Stack.Screen <Stack.Screen
name='Screen-Me-Settings' name='Screen-Me-Settings'
component={ScreenMeSettings} component={ScreenMeSettings}
options={{ options={{
headerTitle: t('headers.me.settings.root') headerTitle: t('meSettings:heading')
}} }}
/> />

View File

@ -6,7 +6,7 @@ import { getLocalUrl } from 'src/utils/slices/instancesSlice'
import Login from './Root/Login' import Login from './Root/Login'
import MyInfo from './Root/MyInfo' import MyInfo from './Root/MyInfo'
import MyCollections from './Root/MyCollections' import Collections from './Root/Collections'
import Settings from './Root/Settings' import Settings from './Root/Settings'
import Logout from './Root/Logout' import Logout from './Root/Logout'
@ -16,7 +16,7 @@ const ScreenMeRoot: React.FC = () => {
return ( return (
<ScrollView> <ScrollView>
{localRegistered ? <MyInfo /> : <Login />} {localRegistered ? <MyInfo /> : <Login />}
{localRegistered && <MyCollections />} {localRegistered && <Collections />}
<Settings /> <Settings />
{localRegistered && <Logout />} {localRegistered && <Logout />}
</ScrollView> </ScrollView>

View File

@ -4,34 +4,34 @@ import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuItem } from 'src/components/Menu'
const MyInfo: React.FC = () => { const Collections: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation('meRoot')
const navigation = useNavigation() const navigation = useNavigation()
return ( return (
<MenuContainer> <MenuContainer>
<MenuItem <MenuItem
iconFront='mail' iconFront='mail'
title={t('headers.me.conversations')} title={t('content.collections.conversations')}
onPress={() => navigation.navigate('Screen-Me-Conversations')} onPress={() => navigation.navigate('Screen-Me-Conversations')}
/> />
<MenuItem <MenuItem
iconFront='bookmark' iconFront='bookmark'
title={t('headers.me.bookmarks')} title={t('content.collections.bookmarks')}
onPress={() => navigation.navigate('Screen-Me-Bookmarks')} onPress={() => navigation.navigate('Screen-Me-Bookmarks')}
/> />
<MenuItem <MenuItem
iconFront='star' iconFront='star'
title={t('headers.me.favourites')} title={t('content.collections.favourites')}
onPress={() => navigation.navigate('Screen-Me-Favourites')} onPress={() => navigation.navigate('Screen-Me-Favourites')}
/> />
<MenuItem <MenuItem
iconFront='list' iconFront='list'
title={t('headers.me.lists.root')} title={t('content.collections.lists')}
onPress={() => navigation.navigate('Screen-Me-Lists')} onPress={() => navigation.navigate('Screen-Me-Lists')}
/> />
</MenuContainer> </MenuContainer>
) )
} }
export default MyInfo export default Collections

View File

@ -5,17 +5,19 @@ import { updateLocal } from 'src/utils/slices/instancesSlice'
import MenuButton from 'src/components/Menu/Button' import MenuButton from 'src/components/Menu/Button'
import { MenuContainer } from 'src/components/Menu' import { MenuContainer } from 'src/components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useTranslation } from 'react-i18next'
const Logout: React.FC = () => { const Logout: React.FC = () => {
const { t } = useTranslation('meRoot')
const dispatch = useDispatch() const dispatch = useDispatch()
const navigation = useNavigation() const navigation = useNavigation()
const alertOption = { const alertOption = {
title: '确认退出登录?', title: t('content.logout.alert.title'),
message: '退出登录后,需要重新认证账号', message: t('content.logout.alert.message'),
buttons: [ buttons: [
{ {
text: '退出登录', text: t('content.logout.alert.buttons.logout'),
style: 'destructive' as const, style: 'destructive' as const,
onPress: () => { onPress: () => {
dispatch(updateLocal({})) dispatch(updateLocal({}))
@ -26,7 +28,7 @@ const Logout: React.FC = () => {
} }
}, },
{ {
text: '取消', text: t('content.logout.alert.buttons.cancel'),
style: 'cancel' as const style: 'cancel' as const
} }
] ]
@ -35,7 +37,7 @@ const Logout: React.FC = () => {
return ( return (
<MenuContainer> <MenuContainer>
<MenuButton <MenuButton
text='退出当前账号' text={t('content.logout.button')}
destructive={true} destructive={true}
alertOption={alertOption} alertOption={alertOption}
/> />

View File

@ -5,14 +5,14 @@ import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuItem } from 'src/components/Menu'
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation('meRoot')
const navigation = useNavigation() const navigation = useNavigation()
return ( return (
<MenuContainer> <MenuContainer>
<MenuItem <MenuItem
iconFront='settings' iconFront='settings'
title={t('headers.me.settings.root')} title={t('content.settings')}
onPress={() => navigation.navigate('Screen-Me-Settings')} onPress={() => navigation.navigate('Screen-Me-Settings')}
/> />
</MenuContainer> </MenuContainer>

View File

@ -6,28 +6,32 @@ import { useDispatch, useSelector } from 'react-redux'
import { MenuContainer, MenuItem } from 'src/components/Menu' import { MenuContainer, MenuItem } from 'src/components/Menu'
import { import {
changeLanguage, changeLanguage,
getSettingsLanguage changeTheme,
getSettingsLanguage,
getSettingsTheme
} from 'src/utils/slices/settingsSlice' } from 'src/utils/slices/settingsSlice'
import { useTheme } from 'src/utils/styles/ThemeManager'
const ScreenMeSettings: React.FC = () => { const ScreenMeSettings: React.FC = () => {
const { t, i18n } = useTranslation('settings') const { t, i18n } = useTranslation('meSettings')
const language = useSelector(getSettingsLanguage) const { setTheme } = useTheme()
const settingsLanguage = useSelector(getSettingsLanguage)
const settingsTheme = useSelector(getSettingsTheme)
const dispatch = useDispatch() const dispatch = useDispatch()
console.log(i18n.language)
return ( return (
<MenuContainer marginTop={true}> <MenuContainer marginTop={true}>
<MenuItem <MenuItem
title={t('content.language.title')} title={t('content.language.heading')}
content={t(`settings:content.language.options.${language}`)} content={t(`content.language.options.${settingsLanguage}`)}
iconBack='chevron-right' iconBack='chevron-right'
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( ActionSheetIOS.showActionSheetWithOptions(
{ {
options: [ options: [
t('settings:content.language.options.zh'), t('content.language.options.zh'),
t('settings:content.language.options.en'), t('content.language.options.en'),
'取消' t('content.language.options.cancel')
], ],
cancelButtonIndex: 2 cancelButtonIndex: 2
}, },
@ -46,6 +50,39 @@ const ScreenMeSettings: React.FC = () => {
) )
} }
/> />
<MenuItem
title={t('content.theme.heading')}
content={t(`content.theme.options.${settingsTheme}`)}
iconBack='chevron-right'
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: [
t('content.theme.options.auto'),
t('content.theme.options.light'),
t('content.theme.options.dark'),
t('content.theme.options.cancel')
],
cancelButtonIndex: 3
},
buttonIndex => {
switch (buttonIndex) {
case 0:
dispatch(changeTheme('auto'))
break
case 1:
dispatch(changeTheme('light'))
setTheme('light')
break
case 2:
dispatch(changeTheme('dark'))
setTheme('dark')
break
}
}
)
}
/>
</MenuContainer> </MenuContainer>
) )
} }

View File

@ -5,10 +5,12 @@ import { RootState } from 'src/store'
export type SettingsState = { export type SettingsState = {
language: 'zh' | 'en' | undefined language: 'zh' | 'en' | undefined
theme: 'light' | 'dark' | 'auto'
} }
const initialState = { const initialState = {
language: undefined language: undefined,
theme: 'auto'
} }
// export const updateLocal = createAsyncThunk( // export const updateLocal = createAsyncThunk(
@ -62,6 +64,12 @@ const settingsSlice = createSlice({
action: PayloadAction<NonNullable<SettingsState['language']>> action: PayloadAction<NonNullable<SettingsState['language']>>
) => { ) => {
state.language = action.payload state.language = action.payload
},
changeTheme: (
state,
action: PayloadAction<NonNullable<SettingsState['theme']>>
) => {
state.theme = action.payload
} }
} }
// extraReducers: builder => { // extraReducers: builder => {
@ -72,6 +80,7 @@ const settingsSlice = createSlice({
}) })
export const getSettingsLanguage = (state: RootState) => state.settings.language export const getSettingsLanguage = (state: RootState) => state.settings.language
export const getSettingsTheme = (state: RootState) => state.settings.theme
export const { changeLanguage } = settingsSlice.actions export const { changeLanguage, changeTheme } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@ -1,39 +1,44 @@
import React, { createContext, useContext, useEffect, useState } from 'react' import React, { createContext, useContext, useEffect, useState } from 'react'
import { Appearance, useColorScheme } from 'react-native-appearance' import { Appearance } from 'react-native-appearance'
import { useSelector } from 'react-redux'
import { ColorDefinitions, getTheme } from 'src/utils/styles/themes' import { ColorDefinitions, getTheme } from 'src/utils/styles/themes'
import { getSettingsTheme } from '../slices/settingsSlice'
const osTheme = Appearance.getColorScheme() as 'light' | 'dark' type ContextType = {
export const ManageThemeContext: React.Context<{
mode: 'light' | 'dark' mode: 'light' | 'dark'
theme: { [key in ColorDefinitions]: string } theme: { [key in ColorDefinitions]: string }
toggle: () => void setTheme: (theme: 'light' | 'dark') => void
}> = createContext({ }
mode: osTheme,
theme: getTheme(osTheme), export const ManageThemeContext = createContext<ContextType>({
toggle: () => {} mode: 'light',
theme: getTheme('light'),
setTheme: () => {}
}) })
export const useTheme = () => useContext(ManageThemeContext) export const useTheme = () => useContext(ManageThemeContext)
const ThemeManager: React.FC = ({ children }) => { const ThemeManager: React.FC = ({ children }) => {
const [mode, setMode] = useState(osTheme) const userTheme = useSelector(getSettingsTheme)
const systemTheme = useColorScheme() const currentMode =
userTheme === 'auto'
? (Appearance.getColorScheme() as 'light' | 'dark')
: userTheme
const toggleTheme = () => { const [mode, setMode] = useState(currentMode)
mode === 'light' ? setMode('dark') : setMode('light')
} const setTheme = (theme: 'light' | 'dark') => setMode(theme)
useEffect(() => { useEffect(() => {
setMode(systemTheme === 'no-preference' ? 'light' : systemTheme) setMode(currentMode)
}, [systemTheme]) }, [currentMode])
return ( return (
<ManageThemeContext.Provider <ManageThemeContext.Provider
value={{ value={{
mode: mode, mode: mode,
theme: getTheme(mode), theme: getTheme(mode),
toggle: toggleTheme setTheme: setTheme
}} }}
> >
{children} {children}