@ledvance/base 1.3.99 → 1.3.101
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/localazy.json +10 -1
- package/package.json +1 -1
- package/src/api/OrientationService.ts +49 -0
- package/src/components/DiySceneItem.tsx +13 -7
- package/src/components/FeatureInfo.tsx +82 -0
- package/src/components/Page.tsx +2 -0
- package/src/components/ldvTopName.tsx +14 -6
- package/src/hooks/Hooks.ts +85 -22
- package/src/i18n/strings.ts +523 -253
- package/translateKey.txt +10 -1
package/localazy.json
CHANGED
|
@@ -1282,7 +1282,16 @@
|
|
|
1282
1282
|
"MATCH:contact_sensor_battery",
|
|
1283
1283
|
"MATCH:siren_settings_antidismantle",
|
|
1284
1284
|
"MATCH:motion_detection_no_safe_mode_subheadline_text",
|
|
1285
|
-
"MATCH:app_navigation_bottom_msg"
|
|
1285
|
+
"MATCH:app_navigation_bottom_msg",
|
|
1286
|
+
"MATCH:internetaccess_timeschedule",
|
|
1287
|
+
"MATCH:chart_legend_consumption",
|
|
1288
|
+
"MATCH:chart_legend_generation",
|
|
1289
|
+
"MATCH:infobutton_timeschedule",
|
|
1290
|
+
"MATCH:infobutton_fixedtimecycle",
|
|
1291
|
+
"MATCH:infobutton_randomtimecycle",
|
|
1292
|
+
"MATCH:infobutton_timer",
|
|
1293
|
+
"MATCH:infobutton_poweronbehavior",
|
|
1294
|
+
"MATCH:infobutton_history"
|
|
1286
1295
|
],
|
|
1287
1296
|
"replacements": {
|
|
1288
1297
|
"REGEX:% %1\\$s.*?\\)%": "{0}",
|
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native'
|
|
2
|
+
import { xLog } from '@utils'
|
|
3
|
+
// 从 NativeModules 中解构出 Orientation,如果不存在,它就是 undefined
|
|
4
|
+
const { Orientation } = NativeModules
|
|
5
|
+
/**
|
|
6
|
+
* 检查 react-native-orientation-locker 的原生模块是否已链接
|
|
7
|
+
* @returns {boolean} 如果已链接则返回 true,否则返回 false
|
|
8
|
+
*/
|
|
9
|
+
const isSupported = (): boolean => !!Orientation
|
|
10
|
+
/**
|
|
11
|
+
* 锁定屏幕到竖屏。
|
|
12
|
+
* 如果模块未链接,此函数将不执行任何操作。
|
|
13
|
+
*/
|
|
14
|
+
const lockToPortrait = (): void => {
|
|
15
|
+
if (isSupported()) {
|
|
16
|
+
Orientation.lockToPortrait()
|
|
17
|
+
} else {
|
|
18
|
+
xLog('[OrientationService] Orientation is not linked. Skipping lockToPortrait.')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 锁定屏幕到横屏。
|
|
23
|
+
* 如果模块未链接,此函数将不执行任何操作。
|
|
24
|
+
*/
|
|
25
|
+
const lockToLandscape = (): void => {
|
|
26
|
+
if (isSupported()) {
|
|
27
|
+
Orientation.lockToLandscape()
|
|
28
|
+
} else {
|
|
29
|
+
xLog('[OrientationService] Orientation is not linked. Skipping lockToLandscape.')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 解除屏幕方向锁定。
|
|
34
|
+
* 如果模块未链接,此函数将不执行任何操作。
|
|
35
|
+
*/
|
|
36
|
+
const unlockAllOrientations = (): void => {
|
|
37
|
+
if (isSupported()) {
|
|
38
|
+
Orientation.unlockAllOrientations()
|
|
39
|
+
} else {
|
|
40
|
+
xLog('[OrientationService] Orientation is not linked. Skipping unlockAllOrientations.')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 导出所有安全的方法
|
|
44
|
+
export const OrientationService = {
|
|
45
|
+
isSupported,
|
|
46
|
+
lockToPortrait,
|
|
47
|
+
lockToLandscape,
|
|
48
|
+
unlockAllOrientations,
|
|
49
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {StyleSheet, Text, View, ViewProps, ViewStyle} from 'react-native';
|
|
3
|
-
import {
|
|
2
|
+
import {StyleSheet, Text, View, ViewProps, ViewStyle, TouchableOpacity, Image} from 'react-native';
|
|
3
|
+
import {Utils} from 'tuya-panel-kit';
|
|
4
4
|
import Card from './Card';
|
|
5
5
|
import Spacer from './Spacer';
|
|
6
6
|
import I18n from '../i18n';
|
|
7
7
|
import ThemeType from '../config/themeType'
|
|
8
|
+
import res from '../res'
|
|
8
9
|
|
|
9
10
|
const cx = Utils.RatioUtils.convertX;
|
|
10
11
|
const { withTheme } = Utils.ThemeUtils
|
|
@@ -46,6 +47,13 @@ const DiySceneItem = (props: DiySceneItemProps) => {
|
|
|
46
47
|
fontFamily: 'helvetica_neue_lt_std_bd',
|
|
47
48
|
lineHeight: cx(20),
|
|
48
49
|
},
|
|
50
|
+
checkbox: {
|
|
51
|
+
width: cx(45),
|
|
52
|
+
height: cx(45),
|
|
53
|
+
marginTop: cx(-5),
|
|
54
|
+
marginBottom: cx(-10),
|
|
55
|
+
fontWeight: 'bold',
|
|
56
|
+
},
|
|
49
57
|
moodTypeItem: {
|
|
50
58
|
flexDirection: 'row',
|
|
51
59
|
},
|
|
@@ -71,11 +79,9 @@ const DiySceneItem = (props: DiySceneItemProps) => {
|
|
|
71
79
|
<Spacer height={cx(16)} />
|
|
72
80
|
<View style={styles.headline}>
|
|
73
81
|
<Text style={styles.headText}>{scene.name}</Text>
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
onValueChange={props.onSwitch}
|
|
78
|
-
/>
|
|
82
|
+
<TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
|
|
83
|
+
<Image source={{ uri: res.ic_check}} width={cx(44)} height={cx(44)} style={[styles.checkbox, { tintColor: props.enable ? props.theme?.icon.primary : props.theme?.icon.disable}]} />
|
|
84
|
+
</TouchableOpacity>
|
|
79
85
|
</View>
|
|
80
86
|
<Spacer />
|
|
81
87
|
<Spacer height={cx(12)} />
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import I18n from '@i18n'
|
|
2
|
+
import res from '@res'
|
|
3
|
+
import React, { useMemo, useState } from 'react'
|
|
4
|
+
import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
|
|
5
|
+
import { Modal, Utils } from 'tuya-panel-kit'
|
|
6
|
+
import ThemeType from '../config/themeType'
|
|
7
|
+
|
|
8
|
+
const { convertX: cx, height, statusBarHeight } = Utils.RatioUtils
|
|
9
|
+
const { withTheme } = Utils.ThemeUtils
|
|
10
|
+
|
|
11
|
+
interface FeatureInfoProps {
|
|
12
|
+
theme?: ThemeType
|
|
13
|
+
title: string
|
|
14
|
+
content: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FeatureInfo = (props: FeatureInfoProps) => {
|
|
18
|
+
const [visible, setVisible] = useState(false)
|
|
19
|
+
|
|
20
|
+
const styles = useMemo(() => getStyles(props.theme), [props.theme])
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View>
|
|
24
|
+
<TouchableOpacity onPress={() => setVisible(true)}>
|
|
25
|
+
<Image source={{ uri: res.ic_info }} style={styles.icon}/>
|
|
26
|
+
</TouchableOpacity>
|
|
27
|
+
<Modal visible={visible} onMaskPress={() => setVisible(false)}>
|
|
28
|
+
<View>
|
|
29
|
+
<View style={styles.header}>
|
|
30
|
+
<View style={{ width: cx(40) }} />
|
|
31
|
+
<Text style={styles.title}>{props.title}</Text>
|
|
32
|
+
<TouchableOpacity onPress={() => setVisible(false)}>
|
|
33
|
+
<Text style={styles.confirm}>{I18n.getLang('home_screen_home_dialog_yes_con')}</Text>
|
|
34
|
+
</TouchableOpacity>
|
|
35
|
+
</View>
|
|
36
|
+
<ScrollView style={styles.contentContainer}>
|
|
37
|
+
<Text style={styles.content}>{props.content}</Text>
|
|
38
|
+
</ScrollView>
|
|
39
|
+
</View>
|
|
40
|
+
</Modal>
|
|
41
|
+
</View>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getStyles = (theme: ThemeType | undefined) => StyleSheet.create({
|
|
46
|
+
icon: {
|
|
47
|
+
width: cx(24),
|
|
48
|
+
height: cx(24),
|
|
49
|
+
marginHorizontal: cx(5),
|
|
50
|
+
tintColor: theme?.global.brand
|
|
51
|
+
},
|
|
52
|
+
header: {
|
|
53
|
+
backgroundColor: theme?.card.head,
|
|
54
|
+
flexDirection: 'row',
|
|
55
|
+
height: cx(60),
|
|
56
|
+
justifyContent: 'space-between',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
borderTopLeftRadius: cx(10),
|
|
59
|
+
borderTopRightRadius: cx(10),
|
|
60
|
+
paddingHorizontal: cx(8)
|
|
61
|
+
},
|
|
62
|
+
title: {
|
|
63
|
+
color: theme?.global.fontColor,
|
|
64
|
+
fontSize: cx(16),
|
|
65
|
+
fontWeight: 'bold'
|
|
66
|
+
},
|
|
67
|
+
confirm: {
|
|
68
|
+
color: theme?.button.primary,
|
|
69
|
+
fontSize: cx(16)
|
|
70
|
+
},
|
|
71
|
+
contentContainer: {
|
|
72
|
+
height: height - statusBarHeight - cx(100),
|
|
73
|
+
paddingHorizontal: cx(16),
|
|
74
|
+
backgroundColor: theme?.global.background
|
|
75
|
+
},
|
|
76
|
+
content: {
|
|
77
|
+
color: theme?.global.fontColor,
|
|
78
|
+
fontSize: cx(14)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export default withTheme(FeatureInfo)
|
package/src/components/Page.tsx
CHANGED
|
@@ -29,6 +29,7 @@ interface PageProps extends PropsWithChildren<ViewProps> {
|
|
|
29
29
|
showGreenery?: boolean
|
|
30
30
|
greeneryIcon?: string | undefined | number
|
|
31
31
|
loading?: boolean
|
|
32
|
+
info?: React.ReactNode
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const Page = (props: PageProps) => {
|
|
@@ -97,6 +98,7 @@ const Page = (props: PageProps) => {
|
|
|
97
98
|
headlineIconContent={props.headlineIconContent}
|
|
98
99
|
headlineTopContent={props.headlineTopContent}
|
|
99
100
|
headlineContent={props.headlineContent}
|
|
101
|
+
info={props.info}
|
|
100
102
|
/>}
|
|
101
103
|
{props.children}
|
|
102
104
|
</View>
|
|
@@ -17,6 +17,7 @@ interface LdvTopNameProps {
|
|
|
17
17
|
headlineIconContent?: React.ReactNode
|
|
18
18
|
headlineTopContent?: React.ReactNode
|
|
19
19
|
headlineContent?: React.ReactNode
|
|
20
|
+
info?: React.ReactNode
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const LdvTopName = (props: LdvTopNameProps) => {
|
|
@@ -28,6 +29,16 @@ const LdvTopName = (props: LdvTopNameProps) => {
|
|
|
28
29
|
marginHorizontal: cx(24),
|
|
29
30
|
marginBottom: cx(12),
|
|
30
31
|
},
|
|
32
|
+
headlineContainer: {
|
|
33
|
+
flexDirection: 'row',
|
|
34
|
+
justifyContent: 'space-between',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
},
|
|
37
|
+
headline: {
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
flex: 1,
|
|
40
|
+
alignItems: 'center'
|
|
41
|
+
},
|
|
31
42
|
title: {
|
|
32
43
|
color: props.theme?.global.brand,
|
|
33
44
|
fontSize: cx(24),
|
|
@@ -38,12 +49,8 @@ const LdvTopName = (props: LdvTopNameProps) => {
|
|
|
38
49
|
{props.headlineTopContent && props.headlineTopContent}
|
|
39
50
|
{!props.headlineTopContent && <Spacer height={cx(38)} />}
|
|
40
51
|
<View
|
|
41
|
-
style={
|
|
42
|
-
|
|
43
|
-
justifyContent: 'space-between',
|
|
44
|
-
alignItems: 'center',
|
|
45
|
-
}}>
|
|
46
|
-
{props.headlineContent ? props.headlineContent : <View style={{ flexDirection: 'row', flex: 1 }}>
|
|
52
|
+
style={styles.headlineContainer}>
|
|
53
|
+
{props.headlineContent ? props.headlineContent : <View style={styles.headline}>
|
|
47
54
|
<Text style={styles.title}>
|
|
48
55
|
{props.title}
|
|
49
56
|
</Text>
|
|
@@ -52,6 +59,7 @@ const LdvTopName = (props: LdvTopNameProps) => {
|
|
|
52
59
|
resizeMode="contain"
|
|
53
60
|
style={{ height: cx(16), width: cx(16), left: 0 }}
|
|
54
61
|
/> || null}
|
|
62
|
+
{props.info && props.info || null}
|
|
55
63
|
</View>}
|
|
56
64
|
{props.rightIcon && <TouchableOpacity
|
|
57
65
|
accessibilityLabel={"RightIcon"}
|
package/src/hooks/Hooks.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { xLog } from '@utils'
|
|
2
|
+
import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native'
|
|
2
3
|
import { Result } from 'models/modules/Result'
|
|
3
4
|
import { useCallback, useEffect, useRef } from 'react'
|
|
5
|
+
import { AppState, AppStateStatus, DeviceEventEmitter, InteractionManager, Platform } from 'react-native'
|
|
4
6
|
|
|
5
7
|
export function createParams<T>(params: T): T {
|
|
6
8
|
return { ...params }
|
|
@@ -12,7 +14,7 @@ export function useParams<T extends any>(): T {
|
|
|
12
14
|
|
|
13
15
|
export function useCurrentPage(routeName: string): boolean {
|
|
14
16
|
const navigation = useNavigation()
|
|
15
|
-
const { index, routes} = navigation.getState()
|
|
17
|
+
const { index, routes } = navigation.getState()
|
|
16
18
|
return routeName === routes[index].name
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -21,44 +23,44 @@ export function useCurrentPage(routeName: string): boolean {
|
|
|
21
23
|
* 用于在指定时间内阻止长连接响应更新界面状态
|
|
22
24
|
*/
|
|
23
25
|
export function useDpResponseValidator(timeoutMs: number = 1000) {
|
|
24
|
-
const dpTimestamps = useRef<{[dpKey: string]: number}>({})
|
|
25
|
-
|
|
26
|
+
const dpTimestamps = useRef<{ [dpKey: string]: number }>({})
|
|
27
|
+
|
|
26
28
|
// dp 响应时间校验函数
|
|
27
29
|
const sendDpWithTimestamps = useCallback((dpKey: string, sendFunc: () => Promise<any>) => {
|
|
28
30
|
// 记录发送时间戳
|
|
29
|
-
const timestamp = Date.now()
|
|
30
|
-
dpTimestamps.current[dpKey] = timestamp
|
|
31
|
+
const timestamp = Date.now()
|
|
32
|
+
dpTimestamps.current[dpKey] = timestamp
|
|
31
33
|
|
|
32
34
|
// 发送命令
|
|
33
|
-
return sendFunc()
|
|
34
|
-
}, [])
|
|
35
|
+
return sendFunc()
|
|
36
|
+
}, [])
|
|
35
37
|
|
|
36
38
|
// 监听 dp 响应,比较时间戳
|
|
37
39
|
const onDpResponse = useCallback((dpKey: string) => {
|
|
38
|
-
const sendTimestamp = dpTimestamps.current[dpKey]
|
|
40
|
+
const sendTimestamp = dpTimestamps.current[dpKey]
|
|
39
41
|
if (sendTimestamp) {
|
|
40
|
-
const responseTimestamp = Date.now()
|
|
41
|
-
const timeDiff = responseTimestamp - sendTimestamp
|
|
42
|
-
|
|
42
|
+
const responseTimestamp = Date.now()
|
|
43
|
+
const timeDiff = responseTimestamp - sendTimestamp
|
|
44
|
+
|
|
43
45
|
if (timeDiff <= timeoutMs) {
|
|
44
46
|
// 指定时间内收到响应,不接收更新
|
|
45
|
-
return true
|
|
47
|
+
return true // 阻止更新
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
return false
|
|
49
|
-
}, [timeoutMs])
|
|
50
|
+
return false // 允许更新
|
|
51
|
+
}, [timeoutMs])
|
|
50
52
|
|
|
51
53
|
// 组件卸载时清理所有记录
|
|
52
54
|
useEffect(() => {
|
|
53
55
|
return () => {
|
|
54
|
-
dpTimestamps.current = {}
|
|
55
|
-
}
|
|
56
|
-
}, [])
|
|
56
|
+
dpTimestamps.current = {}
|
|
57
|
+
}
|
|
58
|
+
}, [])
|
|
57
59
|
|
|
58
60
|
return {
|
|
59
61
|
sendDpWithTimestamps,
|
|
60
62
|
onDpResponse,
|
|
61
|
-
}
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
interface IControlData {
|
|
@@ -82,7 +84,7 @@ interface IControlDataOptions {
|
|
|
82
84
|
* 通用控制支持device和group
|
|
83
85
|
* @param control Tuya Control Formatter
|
|
84
86
|
* @param sendValueFunc send tuya control function
|
|
85
|
-
* @returns
|
|
87
|
+
* @returns
|
|
86
88
|
*/
|
|
87
89
|
export function useControlData(control: any, sendValueFunc: (value: any) => Promise<Result<any>>): (
|
|
88
90
|
controlValue: IControlData,
|
|
@@ -104,10 +106,71 @@ export function useControlData(control: any, sendValueFunc: (value: any) => Prom
|
|
|
104
106
|
value: controlValue.colorMode ? valueConvert(controlValue.value) : 0,
|
|
105
107
|
temperature: !controlValue.colorMode ? temperatureConvert(controlValue.temperature) : 0,
|
|
106
108
|
brightness: !controlValue.colorMode ? brightnessConvert(controlValue.brightness) : 0,
|
|
107
|
-
})
|
|
109
|
+
})
|
|
108
110
|
return await sendValueFunc(value)
|
|
109
111
|
},
|
|
110
112
|
[]
|
|
111
|
-
)
|
|
113
|
+
)
|
|
112
114
|
return setControlData
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 这是一个增强版的 FocusEffect
|
|
119
|
+
* 1. 使用 useCallback 解决重复调用问题
|
|
120
|
+
* 2. [Android] 监听 AppState 解决从原生 Activity 返回不触发的问题
|
|
121
|
+
* 3. [iOS] 监听原生 ViewControllerWillAppear 事件解决从原生 VC 返回不 new的问题
|
|
122
|
+
*/
|
|
123
|
+
export const useSmartFocusEffect = (
|
|
124
|
+
effect: () => void | (() => void),
|
|
125
|
+
deps: any[] = []
|
|
126
|
+
) => {
|
|
127
|
+
const isFocused = useIsFocused()
|
|
128
|
+
const appState = useRef<AppStateStatus>(AppState.currentState)
|
|
129
|
+
const stableEffect = useCallback(effect, deps)
|
|
130
|
+
// 1. react-navigation 内部的焦点处理
|
|
131
|
+
useFocusEffect(
|
|
132
|
+
useCallback(() => {
|
|
133
|
+
const task = InteractionManager.runAfterInteractions(() => {
|
|
134
|
+
stableEffect()
|
|
135
|
+
})
|
|
136
|
+
return () => task.cancel()
|
|
137
|
+
}, [stableEffect])
|
|
138
|
+
)
|
|
139
|
+
// 2. 平台特定的返回事件处理
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
// --- Android 平台方案 ---
|
|
142
|
+
if (Platform.OS === 'android') {
|
|
143
|
+
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
144
|
+
if (
|
|
145
|
+
appState.current.match(/inactive|background/) &&
|
|
146
|
+
nextAppState === 'active' &&
|
|
147
|
+
isFocused
|
|
148
|
+
) {
|
|
149
|
+
xLog('[SmartFocus] App returned from Native/Background on Android')
|
|
150
|
+
stableEffect()
|
|
151
|
+
}
|
|
152
|
+
appState.current = nextAppState
|
|
153
|
+
}
|
|
154
|
+
AppState.addEventListener('change', handleAppStateChange)
|
|
155
|
+
return () => {
|
|
156
|
+
(AppState as any).removeEventListener('change', handleAppStateChange)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// --- iOS 平台方案 ---
|
|
160
|
+
if (Platform.OS === 'ios') {
|
|
161
|
+
const subscription = DeviceEventEmitter.addListener(
|
|
162
|
+
'RNViewControllerWillAppear',
|
|
163
|
+
() => {
|
|
164
|
+
// 同样需要检查 isFocused,确保只有栈顶的 RN 页面响应
|
|
165
|
+
if (isFocused) {
|
|
166
|
+
xLog('[SmartFocus] iOS ViewController will appear')
|
|
167
|
+
stableEffect()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
return () => {
|
|
172
|
+
subscription.remove()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, [isFocused, stableEffect])
|
|
113
176
|
}
|