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 = () => {
return (
<AppearanceProvider>
<ThemeManager>
<ReactQueryCacheProvider queryCache={queryCache}>
<Provider store={store}>
<PersistGate persistor={persistor}>
{bootstrapped => {
if (bootstrapped) {
require('src/i18n/i18n')
return <Index />
} else {
return <></>
}
}}
</PersistGate>
</Provider>
</ReactQueryCacheProvider>
</ThemeManager>
<ReactQueryCacheProvider queryCache={queryCache}>
<Provider store={store}>
<PersistGate persistor={persistor}>
{bootstrapped => {
if (bootstrapped) {
console.log('Bootstrapped!')
require('src/i18n/i18n')
return (
<ThemeManager>
<Index />
</ThemeManager>
)
} else {
return <></>
}
}}
</PersistGate>
</Provider>
</ReactQueryCacheProvider>
</AppearanceProvider>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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'
component={ScreenMeConversations}
options={{
headerTitle: t('headers.me.conversations')
headerTitle: t('meConversations:heading')
}}
/>
<Stack.Screen
name='Screen-Me-Bookmarks'
component={ScreenMeBookmarks}
options={{
headerTitle: t('headers.me.bookmarks')
headerTitle: t('meBookmarks:heading')
}}
/>
<Stack.Screen
name='Screen-Me-Favourites'
component={ScreenMeFavourites}
options={{
headerTitle: t('headers.me.favourites')
headerTitle: t('meFavourites:heading')
}}
/>
<Stack.Screen
name='Screen-Me-Lists'
component={ScreenMeLists}
options={{
headerTitle: t('headers.me.lists.root')
headerTitle: t('meLists:heading')
}}
/>
<Stack.Screen
name='Screen-Me-Lists-List'
component={ScreenMeListsList}
options={({ route }: any) => ({
headerTitle: t('headers.me.lists.list', { list: route.params.title })
headerTitle: t('meListsList:heading', { list: route.params.title })
})}
/>
<Stack.Screen
name='Screen-Me-Settings'
component={ScreenMeSettings}
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 MyInfo from './Root/MyInfo'
import MyCollections from './Root/MyCollections'
import Collections from './Root/Collections'
import Settings from './Root/Settings'
import Logout from './Root/Logout'
@ -16,7 +16,7 @@ const ScreenMeRoot: React.FC = () => {
return (
<ScrollView>
{localRegistered ? <MyInfo /> : <Login />}
{localRegistered && <MyCollections />}
{localRegistered && <Collections />}
<Settings />
{localRegistered && <Logout />}
</ScrollView>

View File

@ -4,34 +4,34 @@ import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuItem } from 'src/components/Menu'
const MyInfo: React.FC = () => {
const { t } = useTranslation()
const Collections: React.FC = () => {
const { t } = useTranslation('meRoot')
const navigation = useNavigation()
return (
<MenuContainer>
<MenuItem
iconFront='mail'
title={t('headers.me.conversations')}
title={t('content.collections.conversations')}
onPress={() => navigation.navigate('Screen-Me-Conversations')}
/>
<MenuItem
iconFront='bookmark'
title={t('headers.me.bookmarks')}
title={t('content.collections.bookmarks')}
onPress={() => navigation.navigate('Screen-Me-Bookmarks')}
/>
<MenuItem
iconFront='star'
title={t('headers.me.favourites')}
title={t('content.collections.favourites')}
onPress={() => navigation.navigate('Screen-Me-Favourites')}
/>
<MenuItem
iconFront='list'
title={t('headers.me.lists.root')}
title={t('content.collections.lists')}
onPress={() => navigation.navigate('Screen-Me-Lists')}
/>
</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 { MenuContainer } from 'src/components/Menu'
import { useNavigation } from '@react-navigation/native'
import { useTranslation } from 'react-i18next'
const Logout: React.FC = () => {
const { t } = useTranslation('meRoot')
const dispatch = useDispatch()
const navigation = useNavigation()
const alertOption = {
title: '确认退出登录?',
message: '退出登录后,需要重新认证账号',
title: t('content.logout.alert.title'),
message: t('content.logout.alert.message'),
buttons: [
{
text: '退出登录',
text: t('content.logout.alert.buttons.logout'),
style: 'destructive' as const,
onPress: () => {
dispatch(updateLocal({}))
@ -26,7 +28,7 @@ const Logout: React.FC = () => {
}
},
{
text: '取消',
text: t('content.logout.alert.buttons.cancel'),
style: 'cancel' as const
}
]
@ -35,7 +37,7 @@ const Logout: React.FC = () => {
return (
<MenuContainer>
<MenuButton
text='退出当前账号'
text={t('content.logout.button')}
destructive={true}
alertOption={alertOption}
/>

View File

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

View File

@ -6,28 +6,32 @@ import { useDispatch, useSelector } from 'react-redux'
import { MenuContainer, MenuItem } from 'src/components/Menu'
import {
changeLanguage,
getSettingsLanguage
changeTheme,
getSettingsLanguage,
getSettingsTheme
} from 'src/utils/slices/settingsSlice'
import { useTheme } from 'src/utils/styles/ThemeManager'
const ScreenMeSettings: React.FC = () => {
const { t, i18n } = useTranslation('settings')
const language = useSelector(getSettingsLanguage)
const { t, i18n } = useTranslation('meSettings')
const { setTheme } = useTheme()
const settingsLanguage = useSelector(getSettingsLanguage)
const settingsTheme = useSelector(getSettingsTheme)
const dispatch = useDispatch()
console.log(i18n.language)
return (
<MenuContainer marginTop={true}>
<MenuItem
title={t('content.language.title')}
content={t(`settings:content.language.options.${language}`)}
title={t('content.language.heading')}
content={t(`content.language.options.${settingsLanguage}`)}
iconBack='chevron-right'
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: [
t('settings:content.language.options.zh'),
t('settings:content.language.options.en'),
'取消'
t('content.language.options.zh'),
t('content.language.options.en'),
t('content.language.options.cancel')
],
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>
)
}

View File

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

View File

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