@onekeyfe/react-native-native-list 3.0.117 → 3.0.119

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.
@@ -628,11 +628,32 @@ function configureWebAvatar(image, source, uri) {
628
628
  });
629
629
  webAvatarCleanup.set(image, dispose);
630
630
  }
631
+ const WEB_IMAGE_FADE_DELAY_MS = 100;
632
+ const WEB_IMAGE_FADE_DURATION_MS = 140;
633
+ function webImageNow(image) {
634
+ return image.ownerDocument.defaultView?.performance.now() ?? Date.now();
635
+ }
636
+ function revealWebImage(element, startedAt) {
637
+ element.getAnimations?.().forEach(animation => animation.cancel());
638
+ element.style.opacity = '1';
639
+ const view = element.ownerDocument.defaultView;
640
+ if (webImageNow(element) - startedAt < WEB_IMAGE_FADE_DELAY_MS || view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches || typeof element.animate !== 'function') return;
641
+ element.animate([{
642
+ opacity: 0
643
+ }, {
644
+ opacity: 1
645
+ }], {
646
+ duration: WEB_IMAGE_FADE_DURATION_MS,
647
+ easing: 'ease-out'
648
+ });
649
+ }
631
650
  function createImage(context, source, className) {
632
651
  const avatarUri = canonicalNativeListAvatarUri(source.uri);
633
652
  const uri = avatarUri ?? safeImageUri(source.uri);
634
653
  if (!uri) return undefined;
635
654
  const image = context.document.createElement('img');
655
+ const startedAt = webImageNow(image);
656
+ image.style.opacity = '0';
636
657
  if (className) image.className = className;
637
658
  // OneKey patch: consume recoverable errors before the visual's final fallback listener.
638
659
  if (avatarUri) {
@@ -646,6 +667,13 @@ function createImage(context, source, className) {
646
667
  image.loading = 'lazy';
647
668
  image.decoding = 'async';
648
669
  image.style.objectFit = source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
670
+ image.addEventListener('load', () => {
671
+ if (image.dataset.nativeListSelectorPaint !== 'true') revealWebImage(image, startedAt);
672
+ });
673
+ image.addEventListener('error', () => {
674
+ image.style.opacity = '0';
675
+ });
676
+ image.dataset.nativeListImageStartedAt = String(startedAt);
649
677
  return image;
650
678
  }
651
679
 
@@ -655,14 +683,18 @@ function paintSelectorImageBackground(image, frame, inset = 0) {
655
683
  const paint = createElement(image.ownerDocument, 'span', 'ok-native-list-selector-image-background');
656
684
  paint.style.cssText = 'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
657
685
  paint.style.inset = String(inset) + 'px';
686
+ paint.style.opacity = '0';
658
687
  paint.style.backgroundSize = image.style.objectFit === 'fill' ? '100% 100%' : image.style.objectFit === 'center' ? 'auto' : image.style.objectFit;
688
+ image.dataset.nativeListSelectorPaint = 'true';
659
689
  image.style.opacity = '0';
660
690
  const update = () => {
661
691
  paint.style.backgroundImage = 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
692
+ revealWebImage(paint, Number(image.dataset.nativeListImageStartedAt) || webImageNow(image));
662
693
  };
663
694
  image.addEventListener('load', update);
664
695
  image.addEventListener('error', () => {
665
696
  paint.style.backgroundImage = 'none';
697
+ paint.style.opacity = '0';
666
698
  });
667
699
  frame.insertBefore(paint, image);
668
700
  if (image.complete && image.naturalWidth > 0) update();
@@ -949,14 +981,28 @@ function createVisual(context, visual, selectorPresentation) {
949
981
  frame.appendChild(fallback);
950
982
  return frame;
951
983
  }
952
- if ('backgroundColor' in visual && visual.backgroundColor) frame.style.background = visual.backgroundColor;
984
+
985
+ // OneKey patch: NativeList images are usually local and should not flash a
986
+ // placeholder. Callers can opt in with image.loadingStrategy.
987
+ const visualBackground = 'backgroundColor' in visual && visual.backgroundColor ? visual.backgroundColor : 'transparent';
953
988
  const source = visual.kind === 'image' ? visual.image : visual.image;
989
+ const showsSourcePlaceholder = !!source && source.loadingStrategy !== undefined && source.loadingStrategy !== 'none';
990
+ frame.style.background = showsSourcePlaceholder ? 'var(--nl-strong)' : visualBackground;
991
+ const fallbackIcon = 'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
992
+ const handlesSourceFallback = showsSourcePlaceholder && (fallbackIcon !== undefined || 'fallbackText' in visual);
954
993
  const image = source ? createImage(context, source) : undefined;
955
994
  if (image) {
956
995
  image.className = 'ok-native-list-visual-main';
957
996
  frame.appendChild(image);
997
+ if (showsSourcePlaceholder) {
998
+ const restoreBackground = () => {
999
+ frame.style.background = visualBackground;
1000
+ };
1001
+ image.addEventListener('load', restoreBackground);
1002
+ if (image.complete && image.naturalWidth > 0) restoreBackground();
1003
+ }
958
1004
  if (selectorPresentation) paintSelectorImageBackground(image, frame);
959
- } else {
1005
+ } else if (!source || showsSourcePlaceholder) {
960
1006
  frame.appendChild(createElement(context.document, 'span', 'ok-native-list-visual-fallback', 'fallbackText' in visual ? visual.fallbackText ?? '' : ''));
961
1007
  }
962
1008
  if (visual.kind === 'token' && visual.networkImage) {
@@ -970,17 +1016,19 @@ function createVisual(context, visual, selectorPresentation) {
970
1016
  frame.appendChild(corner);
971
1017
  }
972
1018
  // OneKey patch: image failure uses the same source-derived fallback as v1.
973
- if ('fallbackIcon' in visual && visual.fallbackIcon) {
974
- const icon = visual.fallbackIcon;
1019
+ const hasFallbackText = 'fallbackText' in visual;
1020
+ if ((fallbackIcon || hasFallbackText) && (!source || handlesSourceFallback)) {
975
1021
  const showFallback = () => {
976
1022
  if (image) {
977
1023
  disposeWebImageRetries(image);
978
1024
  image.remove();
979
1025
  }
980
1026
  frame.querySelector('.ok-native-list-visual-fallback:not(.ok-native-list-visual-corner)')?.remove();
981
- const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback');
982
- applySelectorIcon(fallback, icon.name);
983
- if (icon.tintColor) fallback.style.color = icon.tintColor;
1027
+ const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback', fallbackIcon ? undefined : 'fallbackText' in visual ? visual.fallbackText ?? '' : '');
1028
+ if (fallbackIcon) {
1029
+ applySelectorIcon(fallback, fallbackIcon.name);
1030
+ if (fallbackIcon.tintColor) fallback.style.color = fallbackIcon.tintColor;
1031
+ }
984
1032
  frame.prepend(fallback);
985
1033
  };
986
1034
  if (image) image.addEventListener('error', showFallback, {
@@ -1833,16 +1881,45 @@ function createMarketRow(context, row) {
1833
1881
  titleLine.appendChild(element);
1834
1882
  });
1835
1883
  main.appendChild(titleLine);
1836
- if (row.subtitle || row.subtitleSegments?.length) {
1837
- const subtitle = createElement(context.document, 'span', 'ok-native-list-market-subtitle', row.subtitle);
1838
- applyMarketTextStyle(subtitle, style?.subtitle, {
1839
- fontSize: 14,
1840
- lineHeight: 20,
1841
- weight: 400,
1842
- alignment: 'start'
1843
- });
1844
- applyValueSegments(subtitle, row.subtitleSegments, style?.subtitle?.fontSize ?? 14, style?.subtitle?.lineHeight ?? 20, marketFontWeight(style?.subtitle?.fontWeight, 400));
1845
- main.appendChild(subtitle);
1884
+ if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
1885
+ const subtitleLine = createElement(context.document, 'span', 'ok-native-list-market-subtitle-line');
1886
+ subtitleLine.style.display = 'flex';
1887
+ subtitleLine.style.alignItems = 'center';
1888
+ subtitleLine.style.minWidth = '0';
1889
+ subtitleLine.style.overflow = 'hidden';
1890
+ subtitleLine.style.gap = String(row.subtitlePrefix ? row.subtitlePrefix.gap ?? 4 : 0) + 'px';
1891
+ if (row.subtitlePrefix) {
1892
+ const prefix = createElement(context.document, 'span', 'ok-native-list-market-subtitle-prefix', row.subtitlePrefix.text);
1893
+ applyMarketTextStyle(prefix, row.subtitlePrefix.style, {
1894
+ fontSize: 12,
1895
+ lineHeight: 16,
1896
+ weight: 400,
1897
+ alignment: 'start'
1898
+ });
1899
+ prefix.style.display = 'block';
1900
+ prefix.style.flex = '1 1 auto';
1901
+ prefix.style.minWidth = '0';
1902
+ prefix.style.overflow = 'hidden';
1903
+ prefix.style.textOverflow = 'ellipsis';
1904
+ prefix.style.color = row.subtitlePrefix.style?.color ?? 'var(--nl-secondary)';
1905
+ if (row.subtitlePrefix.maxWidth !== undefined) {
1906
+ prefix.style.maxWidth = String(row.subtitlePrefix.maxWidth) + 'px';
1907
+ }
1908
+ subtitleLine.appendChild(prefix);
1909
+ }
1910
+ if (row.subtitle || row.subtitleSegments?.length) {
1911
+ const subtitle = createElement(context.document, 'span', 'ok-native-list-market-subtitle', row.subtitle);
1912
+ applyMarketTextStyle(subtitle, style?.subtitle, {
1913
+ fontSize: 14,
1914
+ lineHeight: 20,
1915
+ weight: 400,
1916
+ alignment: 'start'
1917
+ });
1918
+ applyValueSegments(subtitle, row.subtitleSegments, style?.subtitle?.fontSize ?? 14, style?.subtitle?.lineHeight ?? 20, marketFontWeight(style?.subtitle?.fontWeight, 400));
1919
+ subtitle.style.flex = row.subtitlePrefix ? '0 0 auto' : '1 1 auto';
1920
+ subtitleLine.appendChild(subtitle);
1921
+ }
1922
+ main.appendChild(subtitleLine);
1846
1923
  }
1847
1924
  body.appendChild(main);
1848
1925
  const trailing = createElement(context.document, 'span', 'ok-native-list-market-trailing');
@@ -2493,8 +2570,25 @@ export class NativeListWebEngine {
2493
2570
  sectionIndexMetrics(viewportHeight) {
2494
2571
  const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2495
2572
  const trackHeight = Math.min(availableHeight, SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length);
2573
+ const centeredOriginY = (viewportHeight - trackHeight) / 2;
2574
+ if (!this.snapshot.capabilities?.sectionIndex?.centeredInWindow) {
2575
+ return {
2576
+ originY: centeredOriginY,
2577
+ trackHeight
2578
+ };
2579
+ }
2580
+ const view = this.document.defaultView;
2581
+ if (!view) return {
2582
+ originY: centeredOriginY,
2583
+ trackHeight
2584
+ };
2585
+ const visualViewport = view.visualViewport;
2586
+ const windowCenterY = visualViewport ? visualViewport.offsetTop + visualViewport.height / 2 : view.innerHeight / 2;
2587
+ const railTop = this.viewportFrame.getBoundingClientRect().top + (Number.parseFloat(this.indexRail.style.top) || 0);
2588
+ const minOriginY = SECTION_INDEX_EDGE_PADDING;
2589
+ const maxOriginY = Math.max(minOriginY, viewportHeight - SECTION_INDEX_EDGE_PADDING - trackHeight);
2496
2590
  return {
2497
- originY: (viewportHeight - trackHeight) / 2,
2591
+ originY: Math.min(maxOriginY, Math.max(minOriginY, windowCenterY - railTop - trackHeight / 2)),
2498
2592
  trackHeight
2499
2593
  };
2500
2594
  }
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
+ import type { ImageSource } from './models';
2
3
  import type { NativeListProps } from './NativeList.types';
3
4
  export type { NativeListProps, NativeListRef } from './NativeList.types';
4
5
  export type { ActionAnchorState, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList.types';
6
+ export declare function preloadNativeListAvatarImages(sources: readonly ImageSource[]): Promise<boolean>;
5
7
  export declare const NativeList: React.ForwardRefExoticComponent<NativeListProps & React.RefAttributes<Readonly<{
6
8
  applySnapshot(snapshot: import("./models").NativeListSnapshot): void;
7
9
  applyPatches(patches: readonly import("./models").RowPatch[]): void;
@@ -1,8 +1,9 @@
1
1
  import React from 'react';
2
2
  import type { NativeListProps } from './NativeList.types';
3
- import type { NativeListSnapshot, RowPatch } from './models';
3
+ import type { ImageSource, NativeListSnapshot, RowPatch } from './models';
4
4
  export type { NativeListProps, NativeListRef } from './NativeList.types';
5
5
  export type { ActionAnchorState, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList.types';
6
+ export declare function preloadNativeListAvatarImages(sources: readonly ImageSource[]): Promise<boolean>;
6
7
  export declare const NativeList: React.ForwardRefExoticComponent<NativeListProps & React.RefAttributes<Readonly<{
7
8
  applySnapshot(snapshot: NativeListSnapshot): void;
8
9
  applyPatches(patches: readonly RowPatch[]): void;
@@ -1,4 +1,4 @@
1
- export { NativeList } from './NativeList';
1
+ export { NativeList, preloadNativeListAvatarImages } from './NativeList';
2
2
  export type { ActionAnchorState, NativeListProps, NativeListRef, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList';
3
3
  export type * from './models';
4
4
  export { applyRowPatches, serializePatches, serializeSnapshot, validatePatches, validateSnapshot, } from './validation';
@@ -19,6 +19,7 @@ export type ImageSource = Readonly<{
19
19
  autoplay?: boolean;
20
20
  optimizeTos?: boolean;
21
21
  overscan?: number;
22
+ /** Defaults to `none` in NativeList; use `static` or `skeleton` to opt into loading UI. */
22
23
  loadingStrategy?: ImageLoadingStrategy;
23
24
  fallbackUri?: string;
24
25
  retryTimes?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.117",
3
+ "version": "3.0.119",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -83,8 +83,8 @@
83
83
  "typescript": "^5.9.2"
84
84
  },
85
85
  "peerDependencies": {
86
- "@onekeyfe/react-native-image": "3.0.117",
87
- "@onekeyfe/react-native-native-logger": "3.0.117",
86
+ "@onekeyfe/react-native-image": "3.0.119",
87
+ "@onekeyfe/react-native-native-logger": "3.0.119",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"
@@ -20,6 +20,7 @@ import type {
20
20
  } from './NativeList.nitro';
21
21
  import type {
22
22
  ActionAnchorInvalidatedEvent,
23
+ ImageSource,
23
24
  RowActionEvent,
24
25
  SelectionDeltaEvent,
25
26
  ReorderEvent,
@@ -64,6 +65,27 @@ const NativeListHost = getHostComponent<
64
65
  NativeListMethods
65
66
  >('NativeList', () => NativeListConfig);
66
67
 
68
+ export function preloadNativeListAvatarImages(
69
+ sources: readonly ImageSource[]
70
+ ): Promise<boolean> {
71
+ return OneKeyImageCache.preload(
72
+ sources.map((source) => ({
73
+ uri: source.uri,
74
+ headers: source.headers,
75
+ resizeWidth: source.width,
76
+ resizeHeight: source.height,
77
+ optimizeTos: source.optimizeTos !== false,
78
+ overscan: source.overscan,
79
+ cachePolicy:
80
+ source.cachePolicy === 'memory'
81
+ ? OneKeyImageCachePolicy.MEMORY
82
+ : source.cachePolicy === 'disk'
83
+ ? OneKeyImageCachePolicy.DISK
84
+ : OneKeyImageCachePolicy.MEMORY_DISK,
85
+ }))
86
+ );
87
+ }
88
+
67
89
  function parsePayload<T>(payloadJson: string): T {
68
90
  return JSON.parse(payloadJson) as T;
69
91
  }
@@ -151,21 +173,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
151
173
  useEffect(() => {
152
174
  avatarLifecycle.current = 'mounted';
153
175
  const queue = new NativeAvatarPrefetchQueue((source) =>
154
- OneKeyImageCache.preload([
155
- {
156
- uri: source.uri,
157
- headers: source.headers,
158
- resizeWidth: source.width,
159
- resizeHeight: source.height,
160
- optimizeTos: false,
161
- cachePolicy:
162
- source.cachePolicy === 'memory'
163
- ? OneKeyImageCachePolicy.MEMORY
164
- : source.cachePolicy === 'disk'
165
- ? OneKeyImageCachePolicy.DISK
166
- : OneKeyImageCachePolicy.MEMORY_DISK,
167
- },
168
- ])
176
+ preloadNativeListAvatarImages([source])
169
177
  );
170
178
  avatarQueueRef.current = queue;
171
179
  updateAvatarPrefetchRef.current();
@@ -9,7 +9,7 @@ import React, {
9
9
  } from 'react';
10
10
  import { View } from 'react-native';
11
11
  import type { NativeListProps, NativeListRef } from './NativeList.types';
12
- import type { NativeListSnapshot, RowPatch } from './models';
12
+ import type { ImageSource, NativeListSnapshot, RowPatch } from './models';
13
13
  import {
14
14
  normalizeIndexScroll,
15
15
  normalizeKeyScroll,
@@ -24,6 +24,10 @@ import {
24
24
  NativeListWebEngine,
25
25
  type NativeListWebCallbacks,
26
26
  } from './web/NativeListWebEngine';
27
+ import {
28
+ acquireNativeListAvatar,
29
+ canonicalNativeListAvatarUri,
30
+ } from './web/NativeListWebAvatarCache';
27
31
 
28
32
  export type { NativeListProps, NativeListRef } from './NativeList.types';
29
33
  export type {
@@ -39,6 +43,39 @@ export type {
39
43
  ScrollToOffsetParams,
40
44
  } from './NativeList.types';
41
45
 
46
+ export function preloadNativeListAvatarImages(
47
+ sources: readonly ImageSource[]
48
+ ): Promise<boolean> {
49
+ if (typeof document === 'undefined') return Promise.resolve(false);
50
+ const uris = [
51
+ ...new Set(
52
+ sources
53
+ .map((source) => canonicalNativeListAvatarUri(source.uri))
54
+ .filter((uri): uri is string => uri !== undefined)
55
+ ),
56
+ ];
57
+ if (!uris.length) return Promise.resolve(false);
58
+ return Promise.all(
59
+ uris.map(
60
+ (uri) =>
61
+ new Promise<boolean>((resolve) => {
62
+ let release: (() => void) | undefined;
63
+ const settle = (success: boolean) => {
64
+ resolve(success);
65
+ queueMicrotask(() => release?.());
66
+ };
67
+ release = acquireNativeListAvatar(
68
+ document,
69
+ uri,
70
+ () => settle(true),
71
+ () => settle(false),
72
+ 0
73
+ );
74
+ })
75
+ )
76
+ ).then((results) => results.every(Boolean));
77
+ }
78
+
42
79
  export const NativeList = forwardRef<NativeListRef, NativeListProps>(
43
80
  function NativeList(
44
81
  {
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { NativeList } from './NativeList';
1
+ export { NativeList, preloadNativeListAvatarImages } from './NativeList';
2
2
  export type {
3
3
  ActionAnchorState,
4
4
  NativeListProps,
package/src/models.ts CHANGED
@@ -21,6 +21,7 @@ export type ImageSource = Readonly<{
21
21
  autoplay?: boolean;
22
22
  optimizeTos?: boolean;
23
23
  overscan?: number;
24
+ /** Defaults to `none` in NativeList; use `static` or `skeleton` to opt into loading UI. */
24
25
  loadingStrategy?: ImageLoadingStrategy;
25
26
  // OneKey patch: fallbackUri is Web-only; retryTimes opts all platforms into terminal retries.
26
27
  fallbackUri?: string;
@@ -1087,6 +1087,30 @@ function configureWebAvatar(
1087
1087
  webAvatarCleanup.set(image, dispose);
1088
1088
  }
1089
1089
 
1090
+ const WEB_IMAGE_FADE_DELAY_MS = 100;
1091
+ const WEB_IMAGE_FADE_DURATION_MS = 140;
1092
+
1093
+ function webImageNow(image: HTMLImageElement): number {
1094
+ return image.ownerDocument.defaultView?.performance.now() ?? Date.now();
1095
+ }
1096
+
1097
+ function revealWebImage(element: HTMLElement, startedAt: number) {
1098
+ element.getAnimations?.().forEach((animation) => animation.cancel());
1099
+ element.style.opacity = '1';
1100
+ const view = element.ownerDocument.defaultView;
1101
+ if (
1102
+ webImageNow(element as HTMLImageElement) - startedAt <
1103
+ WEB_IMAGE_FADE_DELAY_MS ||
1104
+ view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches ||
1105
+ typeof element.animate !== 'function'
1106
+ )
1107
+ return;
1108
+ element.animate([{ opacity: 0 }, { opacity: 1 }], {
1109
+ duration: WEB_IMAGE_FADE_DURATION_MS,
1110
+ easing: 'ease-out',
1111
+ });
1112
+ }
1113
+
1090
1114
  function createImage(
1091
1115
  context: RenderContext,
1092
1116
  source: ImageSource,
@@ -1096,6 +1120,8 @@ function createImage(
1096
1120
  const uri = avatarUri ?? safeImageUri(source.uri);
1097
1121
  if (!uri) return undefined;
1098
1122
  const image = context.document.createElement('img');
1123
+ const startedAt = webImageNow(image);
1124
+ image.style.opacity = '0';
1099
1125
  if (className) image.className = className;
1100
1126
  // OneKey patch: consume recoverable errors before the visual's final fallback listener.
1101
1127
  if (avatarUri) {
@@ -1110,6 +1136,14 @@ function createImage(
1110
1136
  image.decoding = 'async';
1111
1137
  image.style.objectFit =
1112
1138
  source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
1139
+ image.addEventListener('load', () => {
1140
+ if (image.dataset.nativeListSelectorPaint !== 'true')
1141
+ revealWebImage(image, startedAt);
1142
+ });
1143
+ image.addEventListener('error', () => {
1144
+ image.style.opacity = '0';
1145
+ });
1146
+ image.dataset.nativeListImageStartedAt = String(startedAt);
1113
1147
  return image;
1114
1148
  }
1115
1149
 
@@ -1128,20 +1162,27 @@ function paintSelectorImageBackground(
1128
1162
  paint.style.cssText =
1129
1163
  'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
1130
1164
  paint.style.inset = String(inset) + 'px';
1165
+ paint.style.opacity = '0';
1131
1166
  paint.style.backgroundSize =
1132
1167
  image.style.objectFit === 'fill'
1133
1168
  ? '100% 100%'
1134
1169
  : image.style.objectFit === 'center'
1135
1170
  ? 'auto'
1136
1171
  : image.style.objectFit;
1172
+ image.dataset.nativeListSelectorPaint = 'true';
1137
1173
  image.style.opacity = '0';
1138
1174
  const update = () => {
1139
1175
  paint.style.backgroundImage =
1140
1176
  'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
1177
+ revealWebImage(
1178
+ paint,
1179
+ Number(image.dataset.nativeListImageStartedAt) || webImageNow(image)
1180
+ );
1141
1181
  };
1142
1182
  image.addEventListener('load', update);
1143
1183
  image.addEventListener('error', () => {
1144
1184
  paint.style.backgroundImage = 'none';
1185
+ paint.style.opacity = '0';
1145
1186
  });
1146
1187
  frame.insertBefore(paint, image);
1147
1188
  if (image.complete && image.naturalWidth > 0) update();
@@ -1516,15 +1557,38 @@ function createVisual(
1516
1557
  return frame;
1517
1558
  }
1518
1559
 
1519
- if ('backgroundColor' in visual && visual.backgroundColor)
1520
- frame.style.background = visual.backgroundColor;
1560
+ // OneKey patch: NativeList images are usually local and should not flash a
1561
+ // placeholder. Callers can opt in with image.loadingStrategy.
1562
+ const visualBackground =
1563
+ 'backgroundColor' in visual && visual.backgroundColor
1564
+ ? visual.backgroundColor
1565
+ : 'transparent';
1521
1566
  const source = visual.kind === 'image' ? visual.image : visual.image;
1567
+ const showsSourcePlaceholder =
1568
+ !!source &&
1569
+ source.loadingStrategy !== undefined &&
1570
+ source.loadingStrategy !== 'none';
1571
+ frame.style.background = showsSourcePlaceholder
1572
+ ? 'var(--nl-strong)'
1573
+ : visualBackground;
1574
+ const fallbackIcon =
1575
+ 'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
1576
+ const handlesSourceFallback =
1577
+ showsSourcePlaceholder &&
1578
+ (fallbackIcon !== undefined || 'fallbackText' in visual);
1522
1579
  const image = source ? createImage(context, source) : undefined;
1523
1580
  if (image) {
1524
1581
  image.className = 'ok-native-list-visual-main';
1525
1582
  frame.appendChild(image);
1583
+ if (showsSourcePlaceholder) {
1584
+ const restoreBackground = () => {
1585
+ frame.style.background = visualBackground;
1586
+ };
1587
+ image.addEventListener('load', restoreBackground);
1588
+ if (image.complete && image.naturalWidth > 0) restoreBackground();
1589
+ }
1526
1590
  if (selectorPresentation) paintSelectorImageBackground(image, frame);
1527
- } else {
1591
+ } else if (!source || showsSourcePlaceholder) {
1528
1592
  frame.appendChild(
1529
1593
  createElement(
1530
1594
  context.document,
@@ -1556,8 +1620,8 @@ function createVisual(
1556
1620
  frame.appendChild(corner);
1557
1621
  }
1558
1622
  // OneKey patch: image failure uses the same source-derived fallback as v1.
1559
- if ('fallbackIcon' in visual && visual.fallbackIcon) {
1560
- const icon = visual.fallbackIcon;
1623
+ const hasFallbackText = 'fallbackText' in visual;
1624
+ if ((fallbackIcon || hasFallbackText) && (!source || handlesSourceFallback)) {
1561
1625
  const showFallback = () => {
1562
1626
  if (image) {
1563
1627
  disposeWebImageRetries(image);
@@ -1571,10 +1635,18 @@ function createVisual(
1571
1635
  const fallback = createElement(
1572
1636
  context.document,
1573
1637
  'span',
1574
- 'ok-native-list-visual-fallback'
1638
+ 'ok-native-list-visual-fallback',
1639
+ fallbackIcon
1640
+ ? undefined
1641
+ : 'fallbackText' in visual
1642
+ ? visual.fallbackText ?? ''
1643
+ : ''
1575
1644
  );
1576
- applySelectorIcon(fallback, icon.name);
1577
- if (icon.tintColor) fallback.style.color = icon.tintColor;
1645
+ if (fallbackIcon) {
1646
+ applySelectorIcon(fallback, fallbackIcon.name);
1647
+ if (fallbackIcon.tintColor)
1648
+ fallback.style.color = fallbackIcon.tintColor;
1649
+ }
1578
1650
  frame.prepend(fallback);
1579
1651
  };
1580
1652
  if (image) image.addEventListener('error', showFallback, { once: true });
@@ -3254,27 +3326,67 @@ function createMarketRow(context: RenderContext, row: MarketRow): HTMLElement {
3254
3326
  titleLine.appendChild(element);
3255
3327
  });
3256
3328
  main.appendChild(titleLine);
3257
- if (row.subtitle || row.subtitleSegments?.length) {
3258
- const subtitle = createElement(
3329
+ if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
3330
+ const subtitleLine = createElement(
3259
3331
  context.document,
3260
3332
  'span',
3261
- 'ok-native-list-market-subtitle',
3262
- row.subtitle
3263
- );
3264
- applyMarketTextStyle(subtitle, style?.subtitle, {
3265
- fontSize: 14,
3266
- lineHeight: 20,
3267
- weight: 400,
3268
- alignment: 'start',
3269
- });
3270
- applyValueSegments(
3271
- subtitle,
3272
- row.subtitleSegments,
3273
- style?.subtitle?.fontSize ?? 14,
3274
- style?.subtitle?.lineHeight ?? 20,
3275
- marketFontWeight(style?.subtitle?.fontWeight, 400)
3276
- );
3277
- main.appendChild(subtitle);
3333
+ 'ok-native-list-market-subtitle-line'
3334
+ );
3335
+ subtitleLine.style.display = 'flex';
3336
+ subtitleLine.style.alignItems = 'center';
3337
+ subtitleLine.style.minWidth = '0';
3338
+ subtitleLine.style.overflow = 'hidden';
3339
+ subtitleLine.style.gap =
3340
+ String(row.subtitlePrefix ? row.subtitlePrefix.gap ?? 4 : 0) + 'px';
3341
+ if (row.subtitlePrefix) {
3342
+ const prefix = createElement(
3343
+ context.document,
3344
+ 'span',
3345
+ 'ok-native-list-market-subtitle-prefix',
3346
+ row.subtitlePrefix.text
3347
+ );
3348
+ applyMarketTextStyle(prefix, row.subtitlePrefix.style, {
3349
+ fontSize: 12,
3350
+ lineHeight: 16,
3351
+ weight: 400,
3352
+ alignment: 'start',
3353
+ });
3354
+ prefix.style.display = 'block';
3355
+ prefix.style.flex = '1 1 auto';
3356
+ prefix.style.minWidth = '0';
3357
+ prefix.style.overflow = 'hidden';
3358
+ prefix.style.textOverflow = 'ellipsis';
3359
+ prefix.style.color =
3360
+ row.subtitlePrefix.style?.color ?? 'var(--nl-secondary)';
3361
+ if (row.subtitlePrefix.maxWidth !== undefined) {
3362
+ prefix.style.maxWidth = String(row.subtitlePrefix.maxWidth) + 'px';
3363
+ }
3364
+ subtitleLine.appendChild(prefix);
3365
+ }
3366
+ if (row.subtitle || row.subtitleSegments?.length) {
3367
+ const subtitle = createElement(
3368
+ context.document,
3369
+ 'span',
3370
+ 'ok-native-list-market-subtitle',
3371
+ row.subtitle
3372
+ );
3373
+ applyMarketTextStyle(subtitle, style?.subtitle, {
3374
+ fontSize: 14,
3375
+ lineHeight: 20,
3376
+ weight: 400,
3377
+ alignment: 'start',
3378
+ });
3379
+ applyValueSegments(
3380
+ subtitle,
3381
+ row.subtitleSegments,
3382
+ style?.subtitle?.fontSize ?? 14,
3383
+ style?.subtitle?.lineHeight ?? 20,
3384
+ marketFontWeight(style?.subtitle?.fontWeight, 400)
3385
+ );
3386
+ subtitle.style.flex = row.subtitlePrefix ? '0 0 auto' : '1 1 auto';
3387
+ subtitleLine.appendChild(subtitle);
3388
+ }
3389
+ main.appendChild(subtitleLine);
3278
3390
  }
3279
3391
  body.appendChild(main);
3280
3392
  const trailing = createElement(
@@ -4221,8 +4333,29 @@ export class NativeListWebEngine {
4221
4333
  availableHeight,
4222
4334
  SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length
4223
4335
  );
4336
+ const centeredOriginY = (viewportHeight - trackHeight) / 2;
4337
+ if (!this.snapshot.capabilities?.sectionIndex?.centeredInWindow) {
4338
+ return { originY: centeredOriginY, trackHeight };
4339
+ }
4340
+ const view = this.document.defaultView;
4341
+ if (!view) return { originY: centeredOriginY, trackHeight };
4342
+ const visualViewport = view.visualViewport;
4343
+ const windowCenterY = visualViewport
4344
+ ? visualViewport.offsetTop + visualViewport.height / 2
4345
+ : view.innerHeight / 2;
4346
+ const railTop =
4347
+ this.viewportFrame.getBoundingClientRect().top +
4348
+ (Number.parseFloat(this.indexRail.style.top) || 0);
4349
+ const minOriginY = SECTION_INDEX_EDGE_PADDING;
4350
+ const maxOriginY = Math.max(
4351
+ minOriginY,
4352
+ viewportHeight - SECTION_INDEX_EDGE_PADDING - trackHeight
4353
+ );
4224
4354
  return {
4225
- originY: (viewportHeight - trackHeight) / 2,
4355
+ originY: Math.min(
4356
+ maxOriginY,
4357
+ Math.max(minOriginY, windowCenterY - railTop - trackHeight / 2)
4358
+ ),
4226
4359
  trackHeight,
4227
4360
  };
4228
4361
  }