@ccrf01/react-native-template 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +28 -0
- package/.vscode/settings.json +15 -0
- package/README.md +58 -0
- package/package.json +16 -0
- package/template/.bundle/config +2 -0
- package/template/.env.development +1 -0
- package/template/.env.production +1 -0
- package/template/.eslintrc.js +392 -0
- package/template/.java-version +1 -0
- package/template/.prettierrc.js +5 -0
- package/template/.ruby-version +1 -0
- package/template/.watchmanconfig +1 -0
- package/template/Gemfile +23 -0
- package/template/Gemfile.lock +330 -0
- package/template/README.md +97 -0
- package/template/ReactotronConfig.js +17 -0
- package/template/__tests__/App.test.tsx +13 -0
- package/template/_gitignore +75 -0
- package/template/_node-version +1 -0
- package/template/android/app/build.gradle +162 -0
- package/template/android/app/debug.keystore +0 -0
- package/template/android/app/proguard-rules.pro +10 -0
- package/template/android/app/src/debug/AndroidManifest.xml +9 -0
- package/template/android/app/src/main/AndroidManifest.xml +26 -0
- package/template/android/app/src/main/java/com/projectname/MainActivity.kt +30 -0
- package/template/android/app/src/main/java/com/projectname/MainApplication.kt +27 -0
- package/template/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/values/strings.xml +3 -0
- package/template/android/app/src/main/res/values/styles.xml +9 -0
- package/template/android/build.gradle +37 -0
- package/template/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/android/gradle.properties +48 -0
- package/template/android/gradlew +251 -0
- package/template/android/gradlew.bat +99 -0
- package/template/android/keystore/keystore.properties +4 -0
- package/template/android/settings.gradle +6 -0
- package/template/android/version.properties +2 -0
- package/template/app.json +4 -0
- package/template/babel.config.js +38 -0
- package/template/fastlane/Fastfile +136 -0
- package/template/fastlane/Pluginfile +9 -0
- package/template/fastlane/README.md +46 -0
- package/template/index.js +9 -0
- package/template/ios/.xcode.env +11 -0
- package/template/ios/Podfile +35 -0
- package/template/ios/Podfile.lock +3669 -0
- package/template/ios/ProjectName/AppDelegate.swift +48 -0
- package/template/ios/ProjectName/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/template/ios/ProjectName/Images.xcassets/Contents.json +6 -0
- package/template/ios/ProjectName/Info.plist +66 -0
- package/template/ios/ProjectName/LaunchScreen.storyboard +47 -0
- package/template/ios/ProjectName/PrivacyInfo.xcprivacy +47 -0
- package/template/ios/ProjectName.xcodeproj/project.pbxproj +482 -0
- package/template/ios/ProjectName.xcodeproj/xcshareddata/xcschemes/ProjectName.xcscheme +88 -0
- package/template/ios/ProjectName.xcworkspace/contents.xcworkspacedata +10 -0
- package/template/jest.config.js +3 -0
- package/template/metro.config.js +20 -0
- package/template/package-lock.json +17073 -0
- package/template/package.json +156 -0
- package/template/src/@types/emotion.d.ts +12 -0
- package/template/src/@types/env.d.ts +3 -0
- package/template/src/@types/redux-persist-transform-immutable.d.ts +1 -0
- package/template/src/@types/typing.d.ts +17 -0
- package/template/src/AppContainer.tsx +109 -0
- package/template/src/assets/images/143.png +0 -0
- package/template/src/assets/index.ts +13 -0
- package/template/src/components/basic/BackButton/index.tsx +34 -0
- package/template/src/components/basic/Body/index.tsx +68 -0
- package/template/src/components/basic/Button/index.tsx +181 -0
- package/template/src/components/basic/Button/utils.ts +78 -0
- package/template/src/components/basic/ButtonGroup/index.tsx +182 -0
- package/template/src/components/basic/Card/index.tsx +3 -0
- package/template/src/components/basic/Container/index.tsx +38 -0
- package/template/src/components/basic/Content/index.tsx +87 -0
- package/template/src/components/basic/DropDown/index.tsx +354 -0
- package/template/src/components/basic/ExpandableOverlay/index.tsx +113 -0
- package/template/src/components/basic/Header/index.tsx +216 -0
- package/template/src/components/basic/Header/styles.ts +0 -0
- package/template/src/components/basic/Icons/index.tsx +131 -0
- package/template/src/components/basic/InputLabel/index.tsx +19 -0
- package/template/src/components/basic/LoadingOverlay/index.tsx +68 -0
- package/template/src/components/basic/MaterialTextInput/index.tsx +153 -0
- package/template/src/components/basic/NumberInput/index.tsx +53 -0
- package/template/src/components/basic/Picker/PickerContext.ts +7 -0
- package/template/src/components/basic/Picker/PickerHeader.tsx +130 -0
- package/template/src/components/basic/Picker/PickerItem.tsx +105 -0
- package/template/src/components/basic/Picker/PickerItemsList.tsx +135 -0
- package/template/src/components/basic/Picker/PickerPresenter.ts +54 -0
- package/template/src/components/basic/Picker/hooks/useImperativePickerHandle.ts +27 -0
- package/template/src/components/basic/Picker/hooks/usePickerLabel.ts +74 -0
- package/template/src/components/basic/Picker/hooks/usePickerSearch.ts +37 -0
- package/template/src/components/basic/Picker/hooks/usePickerSelection.ts +57 -0
- package/template/src/components/basic/Picker/index.tsx +284 -0
- package/template/src/components/basic/Picker/types.tsx +229 -0
- package/template/src/components/basic/PressableOpacity/index.tsx +20 -0
- package/template/src/components/basic/RootDialog/Dialog.tsx +246 -0
- package/template/src/components/basic/RootDialog/Manager.tsx +110 -0
- package/template/src/components/basic/RootDialog/animations/Animation.ts +29 -0
- package/template/src/components/basic/RootDialog/animations/FadeAnimation.ts +40 -0
- package/template/src/components/basic/RootDialog/animations/ScaleAnimation.ts +37 -0
- package/template/src/components/basic/RootDialog/animations/SlideAnimation.ts +89 -0
- package/template/src/components/basic/RootDialog/components/Backdrop.tsx +60 -0
- package/template/src/components/basic/RootDialog/components/BaseDialog.tsx +564 -0
- package/template/src/components/basic/RootDialog/components/BottomDialog.tsx +32 -0
- package/template/src/components/basic/RootDialog/components/DialogButton.tsx +87 -0
- package/template/src/components/basic/RootDialog/components/DialogContent.tsx +26 -0
- package/template/src/components/basic/RootDialog/components/DialogContext.tsx +8 -0
- package/template/src/components/basic/RootDialog/components/DialogFooter.tsx +42 -0
- package/template/src/components/basic/RootDialog/components/DialogTitle.tsx +53 -0
- package/template/src/components/basic/RootDialog/components/DraggableView.tsx +271 -0
- package/template/src/components/basic/RootDialog/index.ts +21 -0
- package/template/src/components/basic/RootDialog/type.ts +102 -0
- package/template/src/components/basic/Text/index.tsx +8 -0
- package/template/src/components/basic/index.ts +35 -0
- package/template/src/configs/constants/type/APIStatus.type.ts +8 -0
- package/template/src/configs/constants/type/Locale.type.ts +7 -0
- package/template/src/configs/constants/type/StorageKey.type.ts +7 -0
- package/template/src/configs/constants/type/ThemeType.type.ts +6 -0
- package/template/src/configs/constants/type/index.ts +11 -0
- package/template/src/configs/index.ts +22 -0
- package/template/src/contexts/ThemeContext.ts +6 -0
- package/template/src/hooks/useAppLoading.ts +26 -0
- package/template/src/hooks/useAppState.ts +36 -0
- package/template/src/hooks/useArray.ts +47 -0
- package/template/src/hooks/useAsync.ts +42 -0
- package/template/src/hooks/useAsyncStorage.ts +41 -0
- package/template/src/hooks/useBoolean.ts +21 -0
- package/template/src/hooks/useBuildTheme.ts +249 -0
- package/template/src/hooks/useCountDown.ts +111 -0
- package/template/src/hooks/useCounter.ts +27 -0
- package/template/src/hooks/useDebounce.ts +25 -0
- package/template/src/hooks/useDebouncedValidate.ts +32 -0
- package/template/src/hooks/useDebugInformation.ts +38 -0
- package/template/src/hooks/useDeviceToken.ts +20 -0
- package/template/src/hooks/useEncryptedStorage.ts +41 -0
- package/template/src/hooks/useFontFamily.ts +13 -0
- package/template/src/hooks/useHttp.ts +18 -0
- package/template/src/hooks/useInterval.ts +24 -0
- package/template/src/hooks/useIsForeground.ts +17 -0
- package/template/src/hooks/useMMKVStorage.ts +0 -0
- package/template/src/hooks/usePrevious.ts +12 -0
- package/template/src/hooks/useRenderCount.ts +10 -0
- package/template/src/hooks/useTheme.ts +17 -0
- package/template/src/hooks/useTimeCountDown.ts +91 -0
- package/template/src/index.tsx +65 -0
- package/template/src/infrastructures/NetClient/AbstractClient.ts +66 -0
- package/template/src/infrastructures/NetClient/ApiResponse.ts +16 -0
- package/template/src/infrastructures/NetClient/ApisauceClient.ts +76 -0
- package/template/src/infrastructures/NetClient/AxiosClient.ts +80 -0
- package/template/src/infrastructures/NetClient/FetchNetClient.ts +120 -0
- package/template/src/infrastructures/NetClient/config.ts +3 -0
- package/template/src/infrastructures/NetClient/interfaces/INetClient.ts +6 -0
- package/template/src/infrastructures/Storage/IStorage.ts +7 -0
- package/template/src/infrastructures/Storage/MMKVStorage.ts +41 -0
- package/template/src/infrastructures/common/Timeout.ts +27 -0
- package/template/src/infrastructures/common/colorUtils.ts +82 -0
- package/template/src/infrastructures/common/dateUtils.ts +39 -0
- package/template/src/infrastructures/common/logger.ts +115 -0
- package/template/src/locales/en-US/general.json +26 -0
- package/template/src/locales/en-US/index.ts +9 -0
- package/template/src/locales/en-US/screens.json +4 -0
- package/template/src/locales/en-US/setting.json +3 -0
- package/template/src/locales/i18n.ts +109 -0
- package/template/src/locales/zh-TW/general.json +26 -0
- package/template/src/locales/zh-TW/index.ts +9 -0
- package/template/src/locales/zh-TW/screens.json +4 -0
- package/template/src/locales/zh-TW/setting.json +3 -0
- package/template/src/models/index.ts +5 -0
- package/template/src/models/request.model.ts +50 -0
- package/template/src/navigators/DrawerNav/DrawerContent.tsx +66 -0
- package/template/src/navigators/DrawerNav/DrawerItem.tsx +261 -0
- package/template/src/navigators/DrawerNav/index.tsx +39 -0
- package/template/src/navigators/DrawerNav/props.ts +12 -0
- package/template/src/navigators/DrawerNav/types.ts +8 -0
- package/template/src/navigators/MainBottomTabNav/index.tsx +83 -0
- package/template/src/navigators/MainBottomTabNav/props.ts +16 -0
- package/template/src/navigators/MainBottomTabNav/types.ts +6 -0
- package/template/src/navigators/RootStack.tsx +43 -0
- package/template/src/navigators/index.tsx +40 -0
- package/template/src/navigators/props.ts +14 -0
- package/template/src/navigators/types.ts +18 -0
- package/template/src/navigators/utils.ts +68 -0
- package/template/src/redux/api/api.ts +41 -0
- package/template/src/redux/reducers/appSlice.ts +26 -0
- package/template/src/redux/reducers/index.ts +21 -0
- package/template/src/redux/reducers/nonPersistSlice.ts +51 -0
- package/template/src/redux/reducers/settingSlice.ts +48 -0
- package/template/src/redux/reducers/themeSlice.ts +55 -0
- package/template/src/redux/saga/index.ts +5 -0
- package/template/src/redux/saga/settingSaga.ts +21 -0
- package/template/src/redux/selectors/app.ts +9 -0
- package/template/src/redux/selectors/nonPersist.ts +23 -0
- package/template/src/redux/selectors/setting.ts +29 -0
- package/template/src/redux/selectors/theme.ts +13 -0
- package/template/src/redux/store/index.ts +79 -0
- package/template/src/redux/store/types.d.ts +5 -0
- package/template/src/screens/Home/index.tsx +146 -0
- package/template/src/screens/Settings/components/Item.tsx +45 -0
- package/template/src/screens/Settings/index.tsx +97 -0
- package/template/src/screens/Splash/index.tsx +53 -0
- package/template/src/services/Dialogs.tsx +226 -0
- package/template/src/services/PermissionCheck.ts +257 -0
- package/template/src/theme/Common.ts +48 -0
- package/template/src/theme/Fonts.ts +196 -0
- package/template/src/theme/Gutters.ts +63 -0
- package/template/src/theme/Icons.ts +28 -0
- package/template/src/theme/Images.ts +13 -0
- package/template/src/theme/Layout.ts +106 -0
- package/template/src/theme/Variables.ts +167 -0
- package/template/src/theme/components/Buttons.ts +37 -0
- package/template/src/theme/index.ts +8 -0
- package/template/src/theme/metrics.ts +57 -0
- package/template/src/theme/themes/default_dark/Images.ts +7 -0
- package/template/src/theme/themes/default_dark/Variables.ts +84 -0
- package/template/src/theme/themes/default_dark/index.ts +2 -0
- package/template/src/theme/themes/index.ts +8 -0
- package/template/src/theme/types.ts +152 -0
- package/template/src/utils/encrypt-helper.ts +118 -0
- package/template/src/utils/index.ts +76 -0
- package/template/src/utils/sys-info-data.ts +17 -0
- package/template/tsconfig.json +44 -0
- package/template.config.js +8 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { MutableRefObject, useEffect } from 'react'
|
|
2
|
+
import EncryptedStorage from 'react-native-encrypted-storage'
|
|
3
|
+
import useState from 'react-usestateref'
|
|
4
|
+
|
|
5
|
+
import logger from '@/infrastructures/common/logger'
|
|
6
|
+
|
|
7
|
+
const useEncryptedStorage = <T = string>(key: string, initialValue: T): [data: T, setNewData: (value: any) => Promise<void>, retrivedFromStorage: boolean, dataRef: MutableRefObject<T>] => {
|
|
8
|
+
const [data, setData, dataRef] = useState(initialValue)
|
|
9
|
+
const [retrievedFromStorage, setRetrievedFromStorage] = useState(false)
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const init = async () => {
|
|
13
|
+
try {
|
|
14
|
+
const value = await EncryptedStorage.getItem(key)
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
setData((JSON.parse(value) as T) || initialValue)
|
|
17
|
+
} else {
|
|
18
|
+
setData(initialValue)
|
|
19
|
+
}
|
|
20
|
+
setRetrievedFromStorage(true)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// console.error('useAsyncStorage getItem error:', error)
|
|
23
|
+
logger.error('useEncryptedStorage getItem error:', error)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
void init()
|
|
27
|
+
}, [key, initialValue])
|
|
28
|
+
|
|
29
|
+
const setNewData = async (value: T) => {
|
|
30
|
+
try {
|
|
31
|
+
await EncryptedStorage.setItem(key, JSON.stringify(value))
|
|
32
|
+
setData(value)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error('useAsyncStorage setItem error:', error)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [data, setNewData, retrievedFromStorage, dataRef]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default useEncryptedStorage
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSelector } from 'react-redux'
|
|
2
|
+
|
|
3
|
+
import { FONT_FAMILY } from '@/configs'
|
|
4
|
+
import { Locale } from '@/configs/constants/type'
|
|
5
|
+
import { selectLocale } from '@/redux/selectors/setting'
|
|
6
|
+
|
|
7
|
+
const useFontFamily = () => {
|
|
8
|
+
const lang = useSelector(selectLocale)
|
|
9
|
+
|
|
10
|
+
return lang === Locale.zhTW ? FONT_FAMILY.CHI : FONT_FAMILY.ENG
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default useFontFamily
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import AxiosClient from '@/infrastructures/NetClient/AxiosClient'
|
|
4
|
+
import { INetClient } from '@/infrastructures/NetClient/interfaces/INetClient'
|
|
5
|
+
|
|
6
|
+
const makeHttpClinet = (netClientType: string, endpoint: string) => {
|
|
7
|
+
const netClient = new AxiosClient(endpoint)
|
|
8
|
+
|
|
9
|
+
return (path: string) => {
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
return [{}]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
export const useHttp = () => {
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
const useInterval = (callback: () => void, delay: number | null) => {
|
|
3
|
+
const savedCallback = useRef(callback)
|
|
4
|
+
|
|
5
|
+
// Remember the latest callback if it changes.
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
savedCallback.current = callback
|
|
8
|
+
}, [callback])
|
|
9
|
+
|
|
10
|
+
// Set up the interval.
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// Don't schedule if no delay is specified.
|
|
13
|
+
// Note: 0 is a valid value for delay.
|
|
14
|
+
if (!delay && delay !== 0) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const id = setInterval(() => savedCallback.current(), delay)
|
|
19
|
+
|
|
20
|
+
return () => clearInterval(id)
|
|
21
|
+
}, [delay])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default useInterval
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { AppState, AppStateStatus } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export const useIsForeground = (): boolean => {
|
|
5
|
+
const [isForeground, setIsForeground] = useState(true)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const onChange = (state: AppStateStatus): void => {
|
|
9
|
+
setIsForeground(state === 'active')
|
|
10
|
+
}
|
|
11
|
+
const listener = AppState.addEventListener('change', onChange)
|
|
12
|
+
|
|
13
|
+
return () => listener.remove()
|
|
14
|
+
}, [setIsForeground])
|
|
15
|
+
|
|
16
|
+
return isForeground
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { useContext } from 'react'
|
|
3
|
+
|
|
4
|
+
import { ThemeContext } from '@/contexts/ThemeContext'
|
|
5
|
+
|
|
6
|
+
const useTheme = () => {
|
|
7
|
+
const themeContext = _.cloneDeep(useContext(ThemeContext))
|
|
8
|
+
if (!themeContext) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
'No ThemeContext.Provider found when calling useTheme.'
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return themeContext
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default useTheme
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import useBoolean from '@/hooks/useBoolean'
|
|
4
|
+
import useInterval from '@/hooks/useInterval'
|
|
5
|
+
import logger from '@/infrastructures/common/logger'
|
|
6
|
+
|
|
7
|
+
type CountdownControllers = {
|
|
8
|
+
isCountdownRunning: boolean
|
|
9
|
+
startCountdown: () => void
|
|
10
|
+
stopCountdown: () => void
|
|
11
|
+
resetCountdown: () => void
|
|
12
|
+
}
|
|
13
|
+
type CountdownOption = {
|
|
14
|
+
/*
|
|
15
|
+
* The initial value of the countdown in seconds
|
|
16
|
+
*/
|
|
17
|
+
countStart: number
|
|
18
|
+
intervalMs?: number
|
|
19
|
+
isIncrement?: boolean
|
|
20
|
+
countStop?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const useTimeCountDown = (
|
|
24
|
+
countdownOption: CountdownOption,
|
|
25
|
+
): [number, CountdownControllers] => {
|
|
26
|
+
|
|
27
|
+
const { countStart, countStop = 0, intervalMs = 1000, isIncrement = false } = countdownOption
|
|
28
|
+
|
|
29
|
+
const [count, setCount] = useState(countStart)
|
|
30
|
+
const [timestampStart, setTimestampStart] = useState(0)
|
|
31
|
+
// const [lastTimestamp, setLastTimestamp] = useState(0)
|
|
32
|
+
const { setFalse: stopCountdown, setTrue: startCountdown, value: isCountdownRunning } = useBoolean(false)
|
|
33
|
+
|
|
34
|
+
// const runStart = () => {
|
|
35
|
+
// const startTime = Date.now()
|
|
36
|
+
// setTimestampStart(startTime)
|
|
37
|
+
// // setLastTimestamp(startTime)
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isCountdownRunning) {
|
|
42
|
+
const startTime = Date.now()
|
|
43
|
+
setTimestampStart(startTime)
|
|
44
|
+
} else {
|
|
45
|
+
// TODO:
|
|
46
|
+
}
|
|
47
|
+
}, [isCountdownRunning])
|
|
48
|
+
|
|
49
|
+
const resetCounter = () => {
|
|
50
|
+
setCount(countStart)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const countdownCallback = useCallback(() => {
|
|
54
|
+
const timeDiff = Date.now() - timestampStart
|
|
55
|
+
const nextCount = Math.round(!isIncrement ? countStart - timeDiff / 1000 : count + timeDiff / 1000)
|
|
56
|
+
if (!isIncrement) {
|
|
57
|
+
if (nextCount <= countStop) {
|
|
58
|
+
setCount(countStop)
|
|
59
|
+
stopCountdown()
|
|
60
|
+
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
if (nextCount >= countStop) {
|
|
65
|
+
setCount(countStop)
|
|
66
|
+
stopCountdown()
|
|
67
|
+
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
setCount(nextCount)
|
|
72
|
+
}, [count, countStop, timestampStart, isIncrement, stopCountdown])
|
|
73
|
+
|
|
74
|
+
useInterval(countdownCallback, isCountdownRunning ? intervalMs : null)
|
|
75
|
+
|
|
76
|
+
const resetCountdown = () => {
|
|
77
|
+
stopCountdown()
|
|
78
|
+
resetCounter()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
count, {
|
|
83
|
+
isCountdownRunning,
|
|
84
|
+
stopCountdown,
|
|
85
|
+
startCountdown,
|
|
86
|
+
resetCountdown,
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default useTimeCountDown
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NewAppScreen } from '@react-native/new-app-screen'
|
|
2
|
+
import React, { Suspense, useEffect } from 'react'
|
|
3
|
+
import { I18nextProvider } from 'react-i18next'
|
|
4
|
+
import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native'
|
|
5
|
+
import { MD2Colors } from 'react-native-paper'
|
|
6
|
+
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
7
|
+
import { Provider } from 'react-redux'
|
|
8
|
+
import { PersistGate } from 'redux-persist/integration/react'
|
|
9
|
+
|
|
10
|
+
import { persistor, store } from '@/redux/store'
|
|
11
|
+
|
|
12
|
+
import AppContainer from './AppContainer'
|
|
13
|
+
import i18n from './locales/i18n'
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
const isDarkMode = useColorScheme() === 'dark'
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SafeAreaProvider>
|
|
20
|
+
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
|
21
|
+
<I18nextProvider i18n={i18n}>
|
|
22
|
+
<Suspense fallback={'is loading'}>
|
|
23
|
+
<Provider store={store}>
|
|
24
|
+
<PersistGate
|
|
25
|
+
loading={
|
|
26
|
+
<View
|
|
27
|
+
style={{
|
|
28
|
+
flex: 1,
|
|
29
|
+
width: '100%',
|
|
30
|
+
height: '100%',
|
|
31
|
+
backgroundColor: MD2Colors.grey700,
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
}
|
|
35
|
+
persistor={persistor}
|
|
36
|
+
>
|
|
37
|
+
<AppContainer />
|
|
38
|
+
</PersistGate>
|
|
39
|
+
</Provider>
|
|
40
|
+
</Suspense>
|
|
41
|
+
</I18nextProvider>
|
|
42
|
+
</SafeAreaProvider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function AppContent() {
|
|
47
|
+
const safeAreaInsets = useSafeAreaInsets()
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.container}>
|
|
51
|
+
<NewAppScreen
|
|
52
|
+
templateFileName="App.tsx"
|
|
53
|
+
safeAreaInsets={safeAreaInsets}
|
|
54
|
+
/>
|
|
55
|
+
</View>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default App
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ApisauceInstance } from 'apisauce'
|
|
2
|
+
import { AxiosInstance } from 'axios'
|
|
3
|
+
|
|
4
|
+
import { IFetchNetClientInstance } from './FetchNetClient'
|
|
5
|
+
export default class AbstractClient<T extends AxiosInstance|ApisauceInstance|IFetchNetClientInstance> {
|
|
6
|
+
instance: T
|
|
7
|
+
endpoint: string
|
|
8
|
+
constructor(endpoint: string) {
|
|
9
|
+
this.endpoint = endpoint
|
|
10
|
+
this.instance = this.createInstance()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
createInstance():T {
|
|
14
|
+
throw new Error()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected paramsToQueryString = (
|
|
18
|
+
params: Record<string, unknown>,
|
|
19
|
+
skipEmpty = false,
|
|
20
|
+
encodeToken = false,
|
|
21
|
+
) => {
|
|
22
|
+
const paramKeys = Object.keys(params)
|
|
23
|
+
let query = ''
|
|
24
|
+
// const formBody:string[] = [];
|
|
25
|
+
paramKeys.sort()
|
|
26
|
+
paramKeys.forEach((key) => {
|
|
27
|
+
if (
|
|
28
|
+
params[key] !== null &&
|
|
29
|
+
((skipEmpty && params[key] !== '') || !skipEmpty)
|
|
30
|
+
) {
|
|
31
|
+
if (query !== '') {
|
|
32
|
+
query += '&'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (params[key] instanceof Array) {
|
|
36
|
+
const paramsArray: any[] = params[key] as any[]
|
|
37
|
+
paramsArray.sort()
|
|
38
|
+
paramsArray.forEach((param: any) => {
|
|
39
|
+
query +=
|
|
40
|
+
'&' +
|
|
41
|
+
key +
|
|
42
|
+
'=' +
|
|
43
|
+
(encodeToken
|
|
44
|
+
? encodeURI(encodeURIComponent(param))
|
|
45
|
+
: encodeURIComponent(param))
|
|
46
|
+
// query += '&' + key + '=' + encodeURI(encodeURIComponent(param));
|
|
47
|
+
})
|
|
48
|
+
} else {
|
|
49
|
+
const value: string = params[key] as string
|
|
50
|
+
query +=
|
|
51
|
+
key +
|
|
52
|
+
'=' +
|
|
53
|
+
(encodeToken
|
|
54
|
+
? encodeURI(encodeURIComponent(value))
|
|
55
|
+
: encodeURIComponent(value))
|
|
56
|
+
// query += key + '=' + encodeURI(encodeURIComponent(params[key]));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
if (query.startsWith('&')) {
|
|
61
|
+
query = query.substring(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return query
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default class APIResponse<T = any> {
|
|
2
|
+
origResp: unknown
|
|
3
|
+
ok:boolean
|
|
4
|
+
status: number
|
|
5
|
+
headers?: Record<string, unknown>
|
|
6
|
+
data?: T
|
|
7
|
+
error?: Error
|
|
8
|
+
// problem?: string;
|
|
9
|
+
constructor(status: number, headers?: Record<string, unknown>, data?: T, error?: Error, origResp?: unknown) {
|
|
10
|
+
this.status = status
|
|
11
|
+
this.headers = headers
|
|
12
|
+
this.data = data as T
|
|
13
|
+
this.error = error
|
|
14
|
+
this.ok = status >= 200 && status <= 299
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { ApiResponse, ApisauceInstance, create } from 'apisauce'
|
|
3
|
+
import { AxiosError, AxiosResponse } from 'axios'
|
|
4
|
+
|
|
5
|
+
import logger from '../common/logger'
|
|
6
|
+
import AbstractClient from './AbstractClient'
|
|
7
|
+
import APIResponse from './ApiResponse'
|
|
8
|
+
import { API } from './config'
|
|
9
|
+
|
|
10
|
+
export default class ApiSauceClient extends AbstractClient<ApisauceInstance>{
|
|
11
|
+
constructor(endpoint: string) {
|
|
12
|
+
super(endpoint)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createInstance(): ApisauceInstance {
|
|
16
|
+
return create({
|
|
17
|
+
baseURL: this.endpoint,
|
|
18
|
+
timeout: API.Timeout,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public get<T>(path: string, params: Record<string, any>, headerParams: Record<string, string>): Promise<APIResponse<T>> {
|
|
23
|
+
return new Promise<APIResponse<T>>((resolve) => {
|
|
24
|
+
const paramString = this.paramsToQueryString(params)
|
|
25
|
+
const finalPath = path + (paramString.length > 0 ? `?${paramString}` : '')
|
|
26
|
+
logger.request('GET', headerParams, finalPath)
|
|
27
|
+
this.instance.get<T>(finalPath, { headers: headerParams }).then((response: ApiResponse<T>) => {
|
|
28
|
+
logger.resp(response)
|
|
29
|
+
logger.responseBody(response.data as Record<string, unknown>)
|
|
30
|
+
if (response.status && response.status >= 200 && response.status <= 299) {
|
|
31
|
+
const apiResponse = new APIResponse<T>(response.status, response.headers, response.data)
|
|
32
|
+
resolve(apiResponse)
|
|
33
|
+
} else {
|
|
34
|
+
const apiErrorResponse = new APIResponse<T>(response.status ? response.status : -1, response.headers, response.data)
|
|
35
|
+
resolve(apiErrorResponse)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.catch((error: Error) => {
|
|
39
|
+
const axiosErrorResponse = error.hasOwnProperty('response') ? (error as AxiosError<T>).response : undefined
|
|
40
|
+
if (axiosErrorResponse !== undefined) {
|
|
41
|
+
const errorResponse = new APIResponse<T>(axiosErrorResponse.status, axiosErrorResponse.headers, axiosErrorResponse.data, error)
|
|
42
|
+
resolve(errorResponse)
|
|
43
|
+
} else {
|
|
44
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public postJson<T>(path: string, json: Record<string, any>, headerParams: Record<string, string>): Promise<APIResponse<T>> {
|
|
51
|
+
return new Promise<APIResponse<T>>((resolve) => {
|
|
52
|
+
const finalPath = path
|
|
53
|
+
this.instance.post<T>(finalPath, json, { headers: headerParams }).then((response: ApiResponse<T>) => {
|
|
54
|
+
logger.resp(response)
|
|
55
|
+
logger.responseBody(response.data as Record<string, unknown>)
|
|
56
|
+
if (response.status && response.status >= 200 && response.status <= 299) {
|
|
57
|
+
const apiResponse = new APIResponse<T>(response.status, response.headers, response.data)
|
|
58
|
+
resolve(apiResponse)
|
|
59
|
+
} else {
|
|
60
|
+
const apiErrorResponse = new APIResponse<T>(response.status ? response.status : -1, response.headers, response.data)
|
|
61
|
+
resolve(apiErrorResponse)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.catch((error: AxiosError|Error) => {
|
|
65
|
+
const axiosErrorResponse = error.hasOwnProperty('response') ? (error as AxiosError<T>).response : undefined
|
|
66
|
+
if (axiosErrorResponse !== undefined) {
|
|
67
|
+
const errorResponse = new APIResponse<T>(axiosErrorResponse.status, axiosErrorResponse.headers, axiosErrorResponse.data, error)
|
|
68
|
+
resolve(errorResponse)
|
|
69
|
+
} else {
|
|
70
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
|
|
2
|
+
|
|
3
|
+
import logger from '../common/logger'
|
|
4
|
+
import AbstractClient from './AbstractClient'
|
|
5
|
+
import APIResponse from './ApiResponse'
|
|
6
|
+
import { API } from './config'
|
|
7
|
+
import { INetClient } from './interfaces/INetClient'
|
|
8
|
+
|
|
9
|
+
class AxiosClient extends AbstractClient<AxiosInstance> implements INetClient {
|
|
10
|
+
constructor(endpoint: string) {
|
|
11
|
+
super(endpoint)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createInstance(): AxiosInstance {
|
|
15
|
+
return axios.create({
|
|
16
|
+
baseURL: this.endpoint,
|
|
17
|
+
timeout: API.Timeout,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public get<T>(path: string, params: Record<string, unknown>, headerParams: Record<string, string>): Promise<APIResponse<T>> {
|
|
22
|
+
return new Promise<APIResponse<T>>((resolve) => {
|
|
23
|
+
const paramString = this.paramsToQueryString(params)
|
|
24
|
+
const finalPath = path + (paramString.length > 0 ? `?${paramString}` : '')
|
|
25
|
+
logger.request('GET', headerParams, finalPath)
|
|
26
|
+
this.instance.get<T>(finalPath, { headers: headerParams }).then((response: AxiosResponse<T>) => {
|
|
27
|
+
logger.resp(response)
|
|
28
|
+
// logger.responseBody(response.data as Record<string, unknown>)
|
|
29
|
+
if (response.status >= 200 && response.status <= 299) {
|
|
30
|
+
const apiResponse = new APIResponse<T>(response.status, response.headers as Record<string, unknown>, response.data)
|
|
31
|
+
resolve(apiResponse)
|
|
32
|
+
} else {
|
|
33
|
+
const apiErrorResponse = new APIResponse<T>(response.status, response.headers as Record<string, unknown>, response.data)
|
|
34
|
+
resolve(apiErrorResponse)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.catch((error: AxiosError<T>|Error) => {
|
|
38
|
+
logger.log('request error', error)
|
|
39
|
+
const axiosErrorResponse:AxiosResponse<T>|undefined = error.hasOwnProperty('response') ? (error as AxiosError<T>).response : undefined
|
|
40
|
+
|
|
41
|
+
logger.log('request axiosErrorResponse', axiosErrorResponse)
|
|
42
|
+
if (axiosErrorResponse !== undefined) {
|
|
43
|
+
const errorResponse = new APIResponse<T>(axiosErrorResponse.status, axiosErrorResponse.headers as Record<string, unknown>, axiosErrorResponse.data, error)
|
|
44
|
+
resolve(errorResponse)
|
|
45
|
+
} else {
|
|
46
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public postJson<T>(path: string, json: Record<string, unknown>, headerParams: Record<string, string>): Promise<APIResponse<T>> {
|
|
53
|
+
return new Promise<APIResponse<T>>((resolve) => {
|
|
54
|
+
const finalPath = path
|
|
55
|
+
logger.request('POST', headerParams, finalPath)
|
|
56
|
+
this.instance.post<T>(finalPath, json, { headers: headerParams }).then((response: AxiosResponse<T>) => {
|
|
57
|
+
logger.resp(response)
|
|
58
|
+
// logger.responseBody(response.data as Record<string, unknown>)
|
|
59
|
+
if (response.status >= 200 && response.status <= 299) {
|
|
60
|
+
const apiResponse = new APIResponse<T>(response.status, response.headers as Record<string, unknown>, response.data)
|
|
61
|
+
resolve(apiResponse)
|
|
62
|
+
} else {
|
|
63
|
+
const apiErrorResponse = new APIResponse<T>(response.status, response.headers as Record<string, unknown>, response.data)
|
|
64
|
+
resolve(apiErrorResponse)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch((error: AxiosError<T>|Error) => {
|
|
68
|
+
const axiosErrorResponse: AxiosResponse<T>|undefined = error.hasOwnProperty('response') ? (error as AxiosError<T>).response : undefined
|
|
69
|
+
if (axiosErrorResponse !== undefined) {
|
|
70
|
+
const errorResponse = new APIResponse<T>(axiosErrorResponse.status, axiosErrorResponse.headers as Record<string, unknown>, axiosErrorResponse.data, error)
|
|
71
|
+
resolve(errorResponse)
|
|
72
|
+
} else {
|
|
73
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default AxiosClient
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import logger from '../common/logger'
|
|
2
|
+
import Timeout from '../common/Timeout'
|
|
3
|
+
import AbstractClient from './AbstractClient'
|
|
4
|
+
import APIResponse from './ApiResponse'
|
|
5
|
+
import { API } from './config'
|
|
6
|
+
|
|
7
|
+
export interface IFetchNetClientInstance {
|
|
8
|
+
create: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class FetchNetClient extends AbstractClient<IFetchNetClientInstance> {
|
|
12
|
+
constructor(endpoint: string) {
|
|
13
|
+
super(endpoint)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private _request = async (url: string, options: RequestInit): Promise<Response> => {
|
|
17
|
+
return fetch(url, options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private _headersToRecord = (headers: Headers) => {
|
|
21
|
+
const result:Record<string, string> = {}
|
|
22
|
+
headers.forEach((k: string, v: string) => {
|
|
23
|
+
Object.assign(result, { [k]: v })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private _fetchWithTimeout = async (path: string, options: RequestInit, timeout = API.Timeout): Promise<Response> => {
|
|
30
|
+
const timeoutObj = new Timeout<Response>({ ms: timeout })
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
|
|
34
|
+
Promise
|
|
35
|
+
.race([
|
|
36
|
+
this._request(path, options),
|
|
37
|
+
timeoutObj.start(),
|
|
38
|
+
])
|
|
39
|
+
.then((success: Response) => {
|
|
40
|
+
timeoutObj.clear()
|
|
41
|
+
// handle response
|
|
42
|
+
resolve(success)
|
|
43
|
+
}, (error: any) => {
|
|
44
|
+
timeoutObj.clear()
|
|
45
|
+
// handle error
|
|
46
|
+
reject(error)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
createInstance(): IFetchNetClientInstance {
|
|
52
|
+
return {
|
|
53
|
+
create: () => {
|
|
54
|
+
// TODO:
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* REST - GET
|
|
61
|
+
*
|
|
62
|
+
* @param path API Path
|
|
63
|
+
* @param params Query Param
|
|
64
|
+
* @param header API Header
|
|
65
|
+
*/
|
|
66
|
+
public get<T>(path: string, params: Record<string, any>, headerParams: Record<string, string>):Promise<APIResponse<T>> {
|
|
67
|
+
return new Promise<APIResponse<T>>((resolve) => {
|
|
68
|
+
let paramString = '?' + this.paramsToQueryString(params)
|
|
69
|
+
if (paramString.startsWith('&')) paramString = paramString.substring(1)
|
|
70
|
+
if (paramString !== '') paramString = '?' + paramString
|
|
71
|
+
const finalPath = path + paramString
|
|
72
|
+
|
|
73
|
+
logger.request('GET', headerParams, finalPath)
|
|
74
|
+
void this._fetchWithTimeout(finalPath, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: headerParams,
|
|
77
|
+
})
|
|
78
|
+
.then(async (response) => {
|
|
79
|
+
logger.resp(response)
|
|
80
|
+
const headers = this._headersToRecord(response.headers)
|
|
81
|
+
const data: T = await response.json() as T
|
|
82
|
+
|
|
83
|
+
resolve(new APIResponse(response.status, headers, data))
|
|
84
|
+
})
|
|
85
|
+
.catch((error: Error) => {
|
|
86
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* REST - POST => [JSON Body]
|
|
93
|
+
*
|
|
94
|
+
* @param path API Path
|
|
95
|
+
* @param params Body in json format
|
|
96
|
+
* @param header API Header
|
|
97
|
+
*/
|
|
98
|
+
public postJson<T>(path: string, json: Record<string, any>, headerParams: Record<string, string>): Promise<APIResponse<T>> {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
headerParams['Content-Type'] = 'application/json'
|
|
101
|
+
const jsonString = JSON.stringify(json)
|
|
102
|
+
|
|
103
|
+
logger.request('POST', headerParams, path, jsonString)
|
|
104
|
+
void this._fetchWithTimeout(path, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: headerParams,
|
|
107
|
+
body: jsonString,
|
|
108
|
+
})
|
|
109
|
+
.then(async (response) => {
|
|
110
|
+
logger.resp(response)
|
|
111
|
+
const headers = this._headersToRecord(response.headers)
|
|
112
|
+
const data: T = await response.json() as T
|
|
113
|
+
resolve(new APIResponse<T>(response.status, headers, data))
|
|
114
|
+
})
|
|
115
|
+
.catch((error: Error) => {
|
|
116
|
+
resolve(new APIResponse<T>(-1, undefined, undefined, error))
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|