@applicaster/zapp-react-native-utils 15.0.0-alpha.7012242426 → 15.0.0-alpha.7117081265
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/actionsExecutor/ActionExecutorContext.tsx +39 -5
- package/appUtils/localizationsHelper.ts +4 -0
- package/appUtils/playerManager/index.ts +9 -0
- package/appUtils/playerManager/player.ts +1 -1
- package/appUtils/playerManager/usePlayer.tsx +5 -3
- package/cellUtils/__tests__/cellUtils.test.ts +39 -0
- package/cellUtils/index.ts +11 -1
- package/dateUtils/__tests__/dayjs.test.ts +0 -3
- package/dateUtils/index.ts +2 -0
- package/manifestUtils/_internals/__tests__/index.test.js +41 -0
- package/manifestUtils/_internals/index.js +33 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +6 -0
- package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +49 -0
- package/manifestUtils/fieldUtils/index.js +54 -0
- package/manifestUtils/index.js +2 -0
- package/manifestUtils/keys.js +228 -0
- package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +168 -0
- package/manifestUtils/mobileAction/button/index.js +140 -0
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +102 -0
- package/manifestUtils/mobileAction/container/index.js +73 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +127 -0
- package/manifestUtils/mobileAction/groups/defaults.js +76 -0
- package/manifestUtils/mobileAction/groups/index.js +80 -0
- package/numberUtils/__tests__/toNumber.test.ts +27 -12
- package/numberUtils/__tests__/toPositiveNumber.test.ts +32 -4
- package/numberUtils/index.ts +5 -1
- package/package.json +3 -3
- package/pluginUtils/index.ts +4 -5
- package/reactHooks/cell-click/index.ts +2 -1
- package/reactHooks/feed/useInflatedUrl.ts +1 -1
- package/reactHooks/resolvers/useComponentResolver.ts +13 -3
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
resolveObjectValues,
|
|
25
25
|
} from "../appUtils/contextKeysManager/contextResolver";
|
|
26
26
|
import { useNavigation, useRivers } from "../reactHooks";
|
|
27
|
+
import {
|
|
28
|
+
getInflatedDataSourceUrl,
|
|
29
|
+
getSearchContext,
|
|
30
|
+
} from "../reactHooks/feed/useInflatedUrl";
|
|
27
31
|
|
|
28
32
|
import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
|
|
29
33
|
import { useSubscriberFor } from "../reactHooks/useSubscriberFor";
|
|
@@ -104,12 +108,42 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
104
108
|
async (_action, context) => {
|
|
105
109
|
const dispatch = appStore.getDispatch();
|
|
106
110
|
|
|
107
|
-
const
|
|
108
|
-
context?.component?.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
const parentComponent = findParentComponent(
|
|
112
|
+
context?.component?.id,
|
|
113
|
+
context?.screenData
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const componentSource = context?.component?.data?.source;
|
|
117
|
+
|
|
118
|
+
const componentData = componentSource
|
|
119
|
+
? context.component.data
|
|
120
|
+
: parentComponent?.data;
|
|
121
|
+
|
|
122
|
+
const source = componentData?.source;
|
|
123
|
+
const mapping = componentData?.mapping;
|
|
111
124
|
|
|
112
|
-
|
|
125
|
+
let dataSource = source;
|
|
126
|
+
|
|
127
|
+
if (source && mapping) {
|
|
128
|
+
dataSource =
|
|
129
|
+
getInflatedDataSourceUrl({
|
|
130
|
+
source,
|
|
131
|
+
contexts: {
|
|
132
|
+
entry: context?.entryContext,
|
|
133
|
+
screen: context?.screenData,
|
|
134
|
+
search: getSearchContext(null, mapping),
|
|
135
|
+
},
|
|
136
|
+
mapping,
|
|
137
|
+
}) || source;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log_info(`handleAction: refreshComponent for dataSource:${dataSource}`, {
|
|
141
|
+
source,
|
|
142
|
+
inflatedUrl: dataSource,
|
|
143
|
+
mapping,
|
|
144
|
+
entryContextId: context?.entryContext?.id,
|
|
145
|
+
entryId: context?.entry?.id,
|
|
146
|
+
});
|
|
113
147
|
|
|
114
148
|
// TODO: In theory we should wait callback to complete, before completing the action, but now it's not needed
|
|
115
149
|
// TODO: handle focused item removal
|
|
@@ -33,6 +33,7 @@ function deprecationWarning(method) {
|
|
|
33
33
|
export interface PlayerLifecycleListener {
|
|
34
34
|
onRegistered?: (player: Player) => void;
|
|
35
35
|
onUnRegistered?: (player: Player) => void;
|
|
36
|
+
onCastingChanged?: (isCasting: boolean) => void;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
type PlayersMap = {
|
|
@@ -253,10 +254,18 @@ export class PlayerManager {
|
|
|
253
254
|
|
|
254
255
|
public registerCastingReceiver(receiver: Player) {
|
|
255
256
|
this.castingReceiver = receiver;
|
|
257
|
+
|
|
258
|
+
this.lifecycleListeners.forEach((listener) => {
|
|
259
|
+
listener.onCastingChanged?.(true);
|
|
260
|
+
});
|
|
256
261
|
}
|
|
257
262
|
|
|
258
263
|
public unregisterCastingReceiver() {
|
|
259
264
|
this.castingReceiver = null;
|
|
265
|
+
|
|
266
|
+
this.lifecycleListeners.forEach((listener) => {
|
|
267
|
+
listener.onCastingChanged?.(false);
|
|
268
|
+
});
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
/**
|
|
@@ -22,7 +22,6 @@ export const usePlayer = (playerId?: string): Player => {
|
|
|
22
22
|
|
|
23
23
|
const isCasting = playerManager.isCasting();
|
|
24
24
|
|
|
25
|
-
// TODO: We need to prerender when we start/stop casting, fix this
|
|
26
25
|
const getPlayer = (playerId, isCasting) =>
|
|
27
26
|
getControlledPlayer(playerId, isCasting);
|
|
28
27
|
|
|
@@ -46,7 +45,7 @@ export const usePlayer = (playerId?: string): Player => {
|
|
|
46
45
|
return playerManager.addLifecycleListener({
|
|
47
46
|
onRegistered: (player) => {
|
|
48
47
|
if (playerIdToUse === player.playerId) {
|
|
49
|
-
setPlayer(getPlayer(playerIdToUse, isCasting));
|
|
48
|
+
setPlayer(getPlayer(playerIdToUse, playerManager.isCasting()));
|
|
50
49
|
}
|
|
51
50
|
},
|
|
52
51
|
onUnRegistered: (player) => {
|
|
@@ -54,8 +53,11 @@ export const usePlayer = (playerId?: string): Player => {
|
|
|
54
53
|
setPlayer(null);
|
|
55
54
|
}
|
|
56
55
|
},
|
|
56
|
+
onCastingChanged: () => {
|
|
57
|
+
setPlayer(getPlayer(playerIdToUse, playerManager.isCasting()));
|
|
58
|
+
},
|
|
57
59
|
} as PlayerLifecycleListener);
|
|
58
|
-
}, [playerIdToUse
|
|
60
|
+
}, [playerIdToUse]);
|
|
59
61
|
|
|
60
62
|
return player;
|
|
61
63
|
};
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ifEmptyUseFallback,
|
|
4
4
|
textTransform,
|
|
5
5
|
getKeyForLabel,
|
|
6
|
+
getDataTransformForLabel,
|
|
6
7
|
getLabel,
|
|
7
8
|
getAspectRatio,
|
|
8
9
|
getCellWidth,
|
|
@@ -56,6 +57,44 @@ describe("getKeyForLabel", () => {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
describe("getDataTransformForLabel", () => {
|
|
61
|
+
it("returns dataKey when dataKey is not other", () => {
|
|
62
|
+
expect(getDataTransformForLabel(VAL.dataKey, VAL.customKey)).toEqual(
|
|
63
|
+
VAL.dataKey
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns customKey when dataKey is other", () => {
|
|
68
|
+
expect(getDataTransformForLabel(VAL.other, VAL.customKey)).toEqual(
|
|
69
|
+
VAL.customKey
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("preserves full date formats containing dots", () => {
|
|
74
|
+
expect(getDataTransformForLabel(VAL.other, "DD.MM.YYYY")).toEqual(
|
|
75
|
+
"DD.MM.YYYY"
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns default date format when selected key is null or undefined", () => {
|
|
80
|
+
expect(getDataTransformForLabel(EMPTY.nulls, VAL.customKey)).toEqual(
|
|
81
|
+
"DD/MM/YYYY"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(getDataTransformForLabel(EMPTY.undef, VAL.customKey)).toEqual(
|
|
85
|
+
"DD/MM/YYYY"
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(getDataTransformForLabel(VAL.other, EMPTY.nulls)).toEqual(
|
|
89
|
+
"DD/MM/YYYY"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(getDataTransformForLabel(VAL.other, EMPTY.undef)).toEqual(
|
|
93
|
+
"DD/MM/YYYY"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
59
98
|
describe("getLabel", () => {
|
|
60
99
|
it("returns a string from dataKey's path", () => {
|
|
61
100
|
expect(getLabel(VAL.dataKey, VAL.customKey)(entry)).toEqual(entry.id);
|
package/cellUtils/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
|
-
import dayjs from "
|
|
2
|
+
import { dayjs } from "../dateUtils";
|
|
3
3
|
import validateColor from "validate-color";
|
|
4
4
|
|
|
5
5
|
import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
|
|
@@ -31,6 +31,16 @@ export const getKeyForLabel = (dataKey, customKey) => {
|
|
|
31
31
|
return keyToUse.split(".");
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Provides `format` for date formatting. Returns customKey if dataKey is "other", otherwise it returns dataKey.
|
|
36
|
+
* Should consist of valid dayjs tokens - https://day.js.org/docs/en/parse/string-format#list-of-all-available-parsing-tokens
|
|
37
|
+
*/
|
|
38
|
+
export const getDataTransformForLabel = (dataKey, customKey) => {
|
|
39
|
+
const keyToUse = dataKey === CUSTOM_KEY ? customKey : dataKey;
|
|
40
|
+
|
|
41
|
+
return keyToUse ?? "DD/MM/YYYY";
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* This method will return true if the argument passed to it is either empty or nil
|
|
36
46
|
* The method prevents zero from being evaluated as falsey in an || condition
|
package/dateUtils/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ import timezone from "dayjs/plugin/timezone";
|
|
|
4
4
|
import isoWeek from "dayjs/plugin/isoWeek";
|
|
5
5
|
import duration from "dayjs/plugin/duration";
|
|
6
6
|
import relativeTime from "dayjs/plugin/relativeTime";
|
|
7
|
+
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
7
8
|
|
|
8
9
|
// Extend dayjs with plugins
|
|
9
10
|
dayjs.extend(utc);
|
|
10
11
|
dayjs.extend(timezone);
|
|
11
12
|
dayjs.extend(isoWeek);
|
|
12
13
|
dayjs.extend(duration);
|
|
14
|
+
dayjs.extend(customParseFormat);
|
|
13
15
|
|
|
14
16
|
dayjs.extend(relativeTime, {
|
|
15
17
|
thresholds: [
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isKeyHasSuffix,
|
|
3
|
+
isKeyHasAnyOfSuffixes,
|
|
4
|
+
getKeyWithPrefixGenerator,
|
|
5
|
+
} = require("..");
|
|
6
|
+
|
|
7
|
+
describe("manifestUtils/_internals helpers", () => {
|
|
8
|
+
it("checks a key suffix", () => {
|
|
9
|
+
expect(
|
|
10
|
+
isKeyHasSuffix("button_enabled", "mobile_button_1_button_enabled")
|
|
11
|
+
).toBe(true);
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
isKeyHasSuffix("button_enabled", "mobile_button_1_assign_action")
|
|
15
|
+
).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("checks key against multiple suffixes", () => {
|
|
19
|
+
const suffixes = ["font_color", "focused_font_color", "text_transform"];
|
|
20
|
+
|
|
21
|
+
expect(
|
|
22
|
+
isKeyHasAnyOfSuffixes(suffixes, "mobile_button_1_text_transform")
|
|
23
|
+
).toBe(true);
|
|
24
|
+
|
|
25
|
+
expect(
|
|
26
|
+
isKeyHasAnyOfSuffixes(suffixes, "mobile_button_1_asset_alignment")
|
|
27
|
+
).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("generates stable key names using a prefix", () => {
|
|
31
|
+
const withMobileButtonPrefix = getKeyWithPrefixGenerator("mobile_button_2");
|
|
32
|
+
|
|
33
|
+
expect(withMobileButtonPrefix("display_mode")).toBe(
|
|
34
|
+
"mobile_button_2_display_mode"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(withMobileButtonPrefix("asset_enabled")).toBe(
|
|
38
|
+
"mobile_button_2_asset_enabled"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -174,6 +174,36 @@ function generateFieldsFromDefaultsWithoutPrefixedLabel(
|
|
|
174
174
|
)(fields);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Checks whether a generated manifest field key ends with the given suffix.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} suffix
|
|
181
|
+
* @param {string} key
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
const isKeyHasSuffix = (suffix, key) => key.endsWith(`_${suffix}`);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Checks whether a generated manifest field key ends with any supported suffix.
|
|
188
|
+
*
|
|
189
|
+
* @param {string[]} suffixes
|
|
190
|
+
* @param {string} key
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
const isKeyHasAnyOfSuffixes = (suffixes, key) =>
|
|
194
|
+
suffixes.some((suffix) => key.endsWith(`_${suffix}`));
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates a helper that prefixes a manifest field suffix with a shared key stem.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} prefix
|
|
200
|
+
* @returns {(suffix: string) => string}
|
|
201
|
+
*/
|
|
202
|
+
function getKeyWithPrefixGenerator(prefix) {
|
|
203
|
+
// expect prefix as lower snake case, e.g. "mobile_buttons_container"
|
|
204
|
+
return (suffix) => `${prefix}_${suffix}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
177
207
|
module.exports = {
|
|
178
208
|
toSnakeCase,
|
|
179
209
|
toCamelCase,
|
|
@@ -185,4 +215,7 @@ module.exports = {
|
|
|
185
215
|
getDefaultConfiguration,
|
|
186
216
|
compact,
|
|
187
217
|
replaceUnderscoreToSpace,
|
|
218
|
+
isKeyHasSuffix,
|
|
219
|
+
isKeyHasAnyOfSuffixes,
|
|
220
|
+
getKeyWithPrefixGenerator,
|
|
188
221
|
};
|
|
@@ -123,6 +123,12 @@ function getPlayerConfiguration({ platform, version }) {
|
|
|
123
123
|
key: "skip_button_localization_text_skip_intro",
|
|
124
124
|
initial_value: "Skip Intro",
|
|
125
125
|
},
|
|
126
|
+
{
|
|
127
|
+
type: "text_input",
|
|
128
|
+
label: "Stream error message",
|
|
129
|
+
key: "stream_error_message",
|
|
130
|
+
initial_value: "Cannot play stream",
|
|
131
|
+
},
|
|
126
132
|
{
|
|
127
133
|
type: "text_input",
|
|
128
134
|
label: "Locked message",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const {
|
|
2
|
+
withConditional,
|
|
3
|
+
getConditionalKey,
|
|
4
|
+
createConditionalField,
|
|
5
|
+
} = require("..");
|
|
6
|
+
|
|
7
|
+
describe("manifestUtils/fieldUtils", () => {
|
|
8
|
+
it("appends conditions and adds all_conditions when there is more than one condition", () => {
|
|
9
|
+
const config = {
|
|
10
|
+
key: "mobile_button_1_width",
|
|
11
|
+
conditional_fields: [
|
|
12
|
+
{ key: "styles/mobile_button_1_button_enabled", condition_value: true },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const result = withConditional([
|
|
17
|
+
{ key: "styles/mobile_button_1_display_mode", condition_value: "fixed" },
|
|
18
|
+
])(config);
|
|
19
|
+
|
|
20
|
+
expect(result.conditional_fields).toEqual([
|
|
21
|
+
{ key: "styles/mobile_button_1_button_enabled", condition_value: true },
|
|
22
|
+
{ key: "styles/mobile_button_1_display_mode", condition_value: "fixed" },
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
expect(result.rules).toBe("all_conditions");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("creates category-prefixed conditional key", () => {
|
|
29
|
+
expect(
|
|
30
|
+
getConditionalKey("mobile_buttons_container_position", "styles")
|
|
31
|
+
).toBe("styles/mobile_buttons_container_position");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("creates conditional fields with default category and with raw key", () => {
|
|
35
|
+
expect(
|
|
36
|
+
createConditionalField("mobile_button_1_asset_enabled", true)
|
|
37
|
+
).toEqual({
|
|
38
|
+
key: "styles/mobile_button_1_asset_enabled",
|
|
39
|
+
condition_value: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
createConditionalField("mobile_button_1_asset_enabled", true, null)
|
|
44
|
+
).toEqual({
|
|
45
|
+
key: "mobile_button_1_asset_enabled",
|
|
46
|
+
condition_value: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends new conditional_fields from conditions array
|
|
3
|
+
* to config.conditional_fields
|
|
4
|
+
* if there are more than 1 condition, sets rules to "all_conditions"
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<{key: string, condition_value: unknown}>} conditions
|
|
7
|
+
* @returns {(config: object) => object}
|
|
8
|
+
*/
|
|
9
|
+
const withConditional = (conditions) => (config) => {
|
|
10
|
+
const conditional_fields = [
|
|
11
|
+
...(config.conditional_fields || []),
|
|
12
|
+
...conditions,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const next = { ...config, conditional_fields };
|
|
16
|
+
|
|
17
|
+
if (conditional_fields.length > 1) {
|
|
18
|
+
return { ...next, rules: "all_conditions" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return next;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds key for conditional fields prepending category, e.g. "styles/mobile_buttons_container_position"
|
|
26
|
+
*
|
|
27
|
+
* @param {string} key
|
|
28
|
+
* @param {string} category
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function getConditionalKey(key, category) {
|
|
32
|
+
return `${category}/${key}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns a conditional field object for manifest visibility rules.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} key
|
|
39
|
+
* @param {unknown} condition_value
|
|
40
|
+
* @param {string|null} [category="styles"] Pass `null` to use the key as-is.
|
|
41
|
+
* @returns {{key: string, condition_value: unknown}}
|
|
42
|
+
*/
|
|
43
|
+
function createConditionalField(key, condition_value, category = "styles") {
|
|
44
|
+
return {
|
|
45
|
+
key: category ? getConditionalKey(key, category) : key,
|
|
46
|
+
condition_value,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
withConditional,
|
|
52
|
+
getConditionalKey,
|
|
53
|
+
createConditionalField,
|
|
54
|
+
};
|
package/manifestUtils/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
|
|
9
9
|
const { tvActionButtonsContainer } = require("./tvAction/container");
|
|
10
10
|
const { tvActionButton } = require("./tvAction/button");
|
|
11
|
+
const { buildMobileActionButtonGroups } = require("./mobileAction/groups");
|
|
11
12
|
const { compact } = require("./_internals");
|
|
12
13
|
|
|
13
14
|
const { spacingKey, absolutePositionElement } = require("./containers");
|
|
@@ -43,6 +44,7 @@ module.exports = {
|
|
|
43
44
|
tvMenuLabel,
|
|
44
45
|
tvActionButtonsContainer,
|
|
45
46
|
tvActionButton,
|
|
47
|
+
buildMobileActionButtonGroups,
|
|
46
48
|
mobileCellLabel,
|
|
47
49
|
tvCellLabel,
|
|
48
50
|
tvBadges,
|