@fountain-ui/lab 2.0.0-beta.29 → 2.0.0-beta.30

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.
@@ -1,7 +1,13 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { FlatList, ListRenderItem, ViewToken } from 'react-native';
3
3
  import * as R from 'ramda';
4
- import { ComicViewerItemData, default as ComicViewerProps, ErrorInfo } from './ComicViewerProps';
4
+ import {
5
+ ComicViewerItemData,
6
+ ComicViewerItemState,
7
+ default as ComicViewerProps,
8
+ ErrorInfo,
9
+ STATE,
10
+ } from './ComicViewerProps';
5
11
  import type ComicViewerItemProps from './ComicViewerItemProps';
6
12
  import ViewerItem from './ViewerItem';
7
13
 
@@ -9,7 +15,7 @@ const getItemHeights = <T, >(items: ComicViewerItemProps<T>[]): number[] => R.ma
9
15
  const appender = (left: number, right: number): [number, number] => [left + right, left + right];
10
16
  const getHeightAccum = (itemHeights: number[]): [number, number[]] => R.mapAccum(appender, 0, itemHeights);
11
17
 
12
- const keyExtractor = <T, >(item: ComicViewerItemProps<T>) => item.id;
18
+ const keyExtractor = <T, >(item: ComicViewerItemProps<T>) => `${item.sortKey}`;
13
19
 
14
20
  export default function ComicViewer<T>(props: ComicViewerProps<T>) {
15
21
  const {
@@ -20,17 +26,22 @@ export default function ComicViewer<T>(props: ComicViewerProps<T>) {
20
26
  initialScrollPercentage = 0,
21
27
  itemVisiblePercentThreshold = 0,
22
28
  onError,
29
+ onScroll,
30
+ getNextPage,
23
31
  viewerWidth,
24
32
  windowSize = 3,
33
+ pageUnit,
34
+ ListFooterComponent,
25
35
  ...otherProps
26
36
  } = props;
27
37
 
28
38
  const flatListRef = useRef<FlatList>(null);
29
39
 
30
- const errors = useRef<Map<string, number>>(new Map());
40
+ const errors = useRef<Map<number, ErrorInfo>>(new Map());
41
+
31
42
  const debounceTimeOut = useRef<NodeJS.Timeout | null>(null);
32
43
 
33
- const resourceString = R.toString(R.map((itemData: ComicViewerItemData) => itemData.sourceUrl)(data));
44
+ const resourceString = R.toString(R.map((itemData: ComicViewerItemData) => itemData.url)(data));
34
45
 
35
46
  const initialItems = R.map((itemData: ComicViewerItemData<T>) => ({
36
47
  ...itemData,
@@ -41,7 +52,13 @@ export default function ComicViewer<T>(props: ComicViewerProps<T>) {
41
52
 
42
53
  const [items, setItems] = useState<ComicViewerItemProps<T>[]>(initialItems);
43
54
 
44
- const itemHeights = getItemHeights(items);
55
+ const initialItemState: ComicViewerItemState[] = R.map(() => ({
56
+ state: STATE.UNLOAD,
57
+ }))(data);
58
+
59
+ const itemStates = useRef<Array<ComicViewerItemState>>(initialItemState);
60
+
61
+ const itemHeights = [...getItemHeights(items)];
45
62
  const itemHeightAccum = getHeightAccum(itemHeights);
46
63
 
47
64
  const viewabilityConfig = useMemo(() => ({
@@ -62,67 +79,88 @@ export default function ComicViewer<T>(props: ComicViewerProps<T>) {
62
79
  viewableItems: Array<ViewToken>,
63
80
  }) => {
64
81
  setItems((prev: ComicViewerItemProps<T>[]) => {
65
- const viewableItemIds = R.map((viewableItem: ViewToken) => viewableItem.item.id)(viewableItems);
82
+ const viewableItemSortKeys = R.map((viewableItem: ViewToken) => viewableItem.item.sortKey)(viewableItems);
66
83
 
67
- return R.map((prevItem: ComicViewerItemProps<T>) => ({
84
+ const newItems = R.map((prevItem: ComicViewerItemProps<T>) => ({
68
85
  ...prevItem,
69
- isViewable: R.includes(prevItem.id, viewableItemIds),
86
+ isViewable: R.includes(prevItem.sortKey, viewableItemSortKeys),
70
87
  }))([...prev]);
88
+
89
+ return newItems;
71
90
  });
72
91
  });
73
92
 
74
- const onErrorHandler = (errors: ErrorInfo[]) => {
75
- const isRetryLimited = R.any((error: ErrorInfo) => error.count >= errorRetryCount)(errors);
93
+ const itemLoadedHandler = useCallback((sortKey: number) => {
94
+ const itemState = itemStates.current[sortKey - 1];
95
+ itemState.state = STATE.LOADED;
96
+ itemState.error = undefined;
97
+ }, [itemStates]);
76
98
 
77
- if (isRetryLimited) {
99
+ const itemErrorHandler = useCallback((errorInfo: ErrorInfo) => {
100
+ const { sortKey, count } = errorInfo;
101
+
102
+ if (count >= errorRetryCount) {
78
103
  return;
79
104
  }
80
105
 
81
- onError && onError(errors);
82
- };
106
+ errors.current.set(sortKey, errorInfo);
83
107
 
84
- const itemErrorHandler = useCallback((errorInfo: ErrorInfo) => {
85
- errors.current.set(errorInfo.id, errorInfo.count);
108
+ const itemState = itemStates.current[sortKey - 1];
109
+ itemState.state = STATE.FAIL;
110
+ itemState.error = errorInfo;
111
+
112
+ const handleError = () => {
113
+ const errorsArray = Array.from(errors.current.entries());
114
+ const errorsInfo = R.map(([key, value]: [number, ErrorInfo]) => value)(errorsArray);
115
+
116
+ onError && onError([...errorsInfo]);
117
+ errors.current.clear();
118
+ };
86
119
 
87
120
  if (debounceTimeOut.current) {
88
121
  clearTimeout(debounceTimeOut.current);
89
122
  }
90
123
 
91
- debounceTimeOut.current = setTimeout(function () {
92
- const errorsArray = Array.from(errors.current.entries());
93
- const errorsInfo = R.map(([key, value]: [string, number]) => ({
94
- id: key,
95
- count: value,
96
- }))(errorsArray);
97
-
98
- onErrorHandler([...errorsInfo]);
99
- errors.current.clear();
100
- }, errorDebounceMillis);
101
- }, [errorDebounceMillis, errors.current]);
124
+ if (errors.current.size === pageUnit) {
125
+ handleError();
126
+ } else {
127
+ debounceTimeOut.current = setTimeout(handleError, errorDebounceMillis);
128
+ }
129
+ }, [errors.current, itemStates]);
102
130
 
103
131
  const renderItem: ListRenderItem<ComicViewerItemProps<T>> = useCallback(({ item }) => {
132
+ const itemState = itemStates.current[item.sortKey - 1];
133
+
104
134
  const props = {
105
135
  ...item,
136
+ itemState,
137
+ errorRetryCount,
106
138
  onError: itemErrorHandler,
139
+ onLoaded: itemLoadedHandler,
140
+ getNextPage,
107
141
  };
108
142
 
109
143
  return <ViewerItem props={props}/>;
110
- }, []);
144
+ }, [resourceString, itemStates, itemErrorHandler, itemLoadedHandler]);
111
145
 
112
146
  useEffect(() => {
113
147
  setItems((prev: ComicViewerItemProps<T>[]) => {
114
148
  return R.map((prevItem: ComicViewerItemProps<T>) => {
115
- const currentData = R.find((currentItemData: ComicViewerItemData<T>) => prevItem.id === currentItemData.id)(data);
149
+ const currentData = R.find((currentItemData: ComicViewerItemData<T>) => prevItem.sortKey === currentItemData.sortKey)(data);
116
150
 
117
- if (currentData && (currentData.sourceUrl !== prevItem.sourceUrl)) {
151
+ if (currentData
152
+ && itemStates.current[currentData.sortKey - 1].state !== STATE.LOADED
153
+ && (currentData.url !== prevItem.url)) {
118
154
  return {
119
155
  ...prevItem,
120
- sourceUrl: currentData.sourceUrl,
156
+ url: currentData.url,
157
+ expiresAt: currentData.expiresAt,
121
158
  };
122
159
  }
123
160
 
124
161
  return prevItem;
125
162
  })([...prev]);
163
+ ;
126
164
  });
127
165
  }, [resourceString]);
128
166
 
@@ -152,10 +190,12 @@ export default function ComicViewer<T>(props: ComicViewerProps<T>) {
152
190
  initialNumToRender={initialNumToRender}
153
191
  keyExtractor={keyExtractor}
154
192
  onViewableItemsChanged={onViewableItemsChanged.current}
193
+ onScroll={onScroll}
155
194
  ref={flatListRef}
156
195
  renderItem={renderItem}
157
196
  viewabilityConfig={viewabilityConfig}
158
197
  windowSize={windowSize}
198
+ ListFooterComponent={ListFooterComponent}
159
199
  {...otherProps}
160
200
  />
161
201
  );
@@ -1,4 +1,4 @@
1
- import { ComicViewerItemData, ErrorInfo } from './ComicViewerProps';
1
+ import { ComicViewerItemData, ErrorInfo, ComicViewerItemState } from './ComicViewerProps';
2
2
 
3
3
  type ComicViewerItemProps<T> = ComicViewerItemData<T> & {
4
4
  /**
@@ -6,10 +6,25 @@ type ComicViewerItemProps<T> = ComicViewerItemData<T> & {
6
6
  */
7
7
  isViewable: boolean;
8
8
 
9
+ /**
10
+ * How many times retry onError when same item error occur
11
+ * @default 3
12
+ */
13
+ errorRetryCount?: number;
14
+
9
15
  /**
10
16
  * Error handler
11
17
  */
12
18
  onError?: (errorInfo: ErrorInfo) => void;
19
+
20
+ /**
21
+ * Load handler
22
+ */
23
+ onLoaded?: (sortKey: number) => void;
24
+
25
+ getNextPage?: (sortKey: number) => void;
26
+
27
+ itemState?: ComicViewerItemState;
13
28
  }
14
29
 
15
30
  export default ComicViewerItemProps;
@@ -1,15 +1,43 @@
1
+ import React from 'react';
1
2
  import { ComponentProps } from '@fountain-ui/core';
3
+ import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
4
+
5
+ export const STATE = {
6
+ UNLOAD: 'unload',
7
+ LOADING: 'loading',
8
+ LOADED: 'loaded',
9
+ FAIL: 'fail',
10
+ } as const;
11
+
12
+ export type LoadingState = typeof STATE[keyof typeof STATE];
13
+
14
+ export interface ComicViewerItemState{
15
+ /**
16
+ * Content's loading state.
17
+ */
18
+ state: LoadingState,
19
+
20
+ /***
21
+ * Content's error Info.
22
+ */
23
+ error?: ErrorInfo,
24
+ }
2
25
 
3
26
  export interface ErrorInfo {
4
27
  /**
5
- * ComicViewerItemData.id.
28
+ * ComicViewerItemData.sortKey.
6
29
  */
7
- id: string;
30
+ sortKey: number;
8
31
 
9
32
  /**
10
33
  * Number of times an error occurred.
11
34
  */
12
35
  count: number;
36
+
37
+ /**
38
+ * Content is Expired: true
39
+ */
40
+ expired: boolean;
13
41
  }
14
42
 
15
43
  export type ComicViewerItemData<T = {}> = T & {
@@ -21,17 +49,27 @@ export type ComicViewerItemData<T = {}> = T & {
21
49
  /**
22
50
  * Unique value for identifying.
23
51
  */
24
- id: string;
52
+ id: number | undefined;
25
53
 
26
54
  /**
27
55
  * Image sourceUrl for displaying.
28
56
  */
29
- sourceUrl: string;
57
+ url: string;
30
58
 
31
59
  /**
32
60
  * Image width.
33
61
  */
34
62
  width: number;
63
+
64
+ /**
65
+ * SortKey
66
+ */
67
+ sortKey: number;
68
+
69
+ /**
70
+ * Image expire date.
71
+ */
72
+ expiresAt: string;
35
73
  }
36
74
 
37
75
  export default interface ComicViewerProps<T> extends ComponentProps <{
@@ -52,7 +90,7 @@ export default interface ComicViewerProps<T> extends ComponentProps <{
52
90
  */
53
91
  errorRetryCount?: number;
54
92
 
55
- /**
93
+ /**
56
94
  * How many items to render in the initial batch.
57
95
  * @default 1
58
96
  */
@@ -71,6 +109,28 @@ export default interface ComicViewerProps<T> extends ComponentProps <{
71
109
  */
72
110
  itemVisiblePercentThreshold?: number;
73
111
 
112
+ /**
113
+ * Comic viewer width.
114
+ */
115
+ viewerWidth: number;
116
+
117
+ /**
118
+ * The value for FlatList windowSize.
119
+ * @default 3
120
+ */
121
+ windowSize?: number;
122
+
123
+ /**
124
+ * How many images in one page.
125
+ */
126
+ pageUnit: number;
127
+
128
+ /**
129
+ * Method for getting next page contents.
130
+ * @param sortKey
131
+ */
132
+ getNextPage?: (sortKey: number) => void;
133
+
74
134
  /**
75
135
  * Handling all viewerItem errors at once.
76
136
  * @param errors Array of ViewerItems errorInfo.
@@ -78,13 +138,13 @@ export default interface ComicViewerProps<T> extends ComponentProps <{
78
138
  onError?: (errors: ErrorInfo[]) => void;
79
139
 
80
140
  /**
81
- * Comic viewer width.
141
+ * Handle scroll event.
142
+ * @param event Scroll event.
82
143
  */
83
- viewerWidth: number;
144
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
84
145
 
85
146
  /**
86
- * The value for FlatList windowSize.
87
- * @default 3
147
+ * Component for comic viewer footer.
88
148
  */
89
- windowSize?: number;
149
+ ListFooterComponent?: React.ReactElement;
90
150
  }> {}
@@ -1,64 +1,144 @@
1
- import React, { useCallback, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { View } from 'react-native';
3
- import { Image, StyleSheet } from '@fountain-ui/core';
3
+ import * as R from 'ramda';
4
+ import type { PlaceholderProps } from '@fountain-ui/core';
5
+ import { IconButton, Image, Spacer, useTheme } from '@fountain-ui/core';
6
+ import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
7
+ import { Restart } from '@fountain-ui/icons';
4
8
  import ComicViewerItemProps from './ComicViewerItemProps';
5
9
 
6
- const styles = StyleSheet.create({
7
- placeholder: {
8
- backgroundColor: '#abcabc',
9
- },
10
- });
10
+ type PlaceholderStyles = NamedStylesStringUnion<'init' | 'failed' | 'reload'>;
11
+
12
+ const useStyles: UseStyles<PlaceholderStyles> = function (): PlaceholderStyles {
13
+ const theme = useTheme();
14
+
15
+ return {
16
+ init: {
17
+ backgroundColor: theme.palette.paper.grey,
18
+ },
19
+ failed: {
20
+ backgroundColor: theme.palette.paper.grey,
21
+ },
22
+ reload: {
23
+ backgroundColor: theme.palette.paper.grey,
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ },
27
+ };
28
+ };
11
29
 
12
30
  function ViewerItem<T>({ props }: { props: ComicViewerItemProps<T> }) {
13
31
  const {
32
+ expiresAt,
33
+ errorRetryCount = 3,
14
34
  height,
15
- id,
35
+ itemState,
16
36
  isViewable,
17
- onError,
18
- sourceUrl,
37
+ sortKey,
38
+ url,
19
39
  width,
40
+ getNextPage,
41
+ onError,
42
+ onLoaded,
20
43
  } = props;
21
44
 
22
45
  const [isLoaded, setIsLoaded] = useState(false);
23
46
 
24
- const errorCount = useRef<number>(0);
47
+ const styles = useStyles();
48
+
49
+ const errorCount = useRef<number>(R.defaultTo(0)(itemState?.error?.count));
25
50
 
26
51
  const onLoad = useCallback(() => {
27
52
  errorCount.current = 0;
53
+
28
54
  setIsLoaded(true);
29
- }, []);
55
+
56
+ onLoaded && onLoaded(sortKey);
57
+ }, [sortKey]);
30
58
 
31
59
  const handleError = useCallback(() => {
32
60
  errorCount.current = errorCount.current + 1;
33
61
 
62
+ const expired = expiresAt ? new Date(expiresAt) <= new Date() : false;
63
+
64
+ onError && onError({
65
+ sortKey,
66
+ count: errorCount.current,
67
+ expired,
68
+ });
69
+ }, [errorCount.current]);
70
+
71
+ const onReloadPress = useCallback(() => {
72
+ errorCount.current = 1;
73
+
34
74
  onError && onError({
35
- id,
36
- count: errorCount.current
75
+ sortKey,
76
+ count: errorCount.current,
77
+ expired: false,
37
78
  });
38
- }, [id]);
79
+ }, [sortKey]);
39
80
 
40
81
  const viewStyle = { width, height };
41
82
 
42
- const Placeholder = () => (
43
- <View style={[
44
- viewStyle,
45
- styles.placeholder,
46
- ]}/>
47
- );
83
+ const Placeholder = useCallback((props: PlaceholderProps) => {
84
+ const { children, failed } = props;
48
85
 
49
- if (!isViewable && !isLoaded) {
50
- return <Placeholder/>;
51
- }
86
+ if (!isViewable && !isLoaded) {
87
+ return <View style={[
88
+ viewStyle,
89
+ styles.init,
90
+ ]}/>;
91
+ }
92
+
93
+ if (errorCount.current >= errorRetryCount) {
94
+ return <View style={[
95
+ viewStyle,
96
+ styles.reload,
97
+ ]}>
98
+ <Spacer size={20}/>
99
+
100
+ <IconButton
101
+ children={<Restart fill={'#ffffff'}/>}
102
+ style={{
103
+ width: 48,
104
+ height: 48,
105
+ borderRadius: 24,
106
+ color: '#ffffff',
107
+ backgroundColor: '#767676',
108
+ }}
109
+ onPress={onReloadPress}
110
+ />
111
+ </View>;
112
+ }
113
+
114
+ if (failed) {
115
+ return (
116
+ <View style={[
117
+ viewStyle,
118
+ styles.failed,
119
+ ]}/>
120
+ );
121
+ }
122
+
123
+ return children ? children : null;
124
+ }, [isViewable, isLoaded, errorCount.current, url]);
125
+
126
+ useEffect(() => {
127
+ if (url === '') {
128
+ getNextPage?.(sortKey);
129
+ }
130
+ }, []);
52
131
 
53
132
  return (
54
133
  <Image
55
134
  disableOutline={true}
56
- key={sourceUrl}
135
+ key={sortKey}
57
136
  onLoad={onLoad}
58
137
  onError={handleError}
59
- source={{ uri: sourceUrl }}
138
+ source={{ uri: url }}
60
139
  style={viewStyle}
61
140
  square={true}
141
+ Placeholder={Placeholder}
62
142
  />
63
143
  );
64
144
  }
@@ -68,7 +148,7 @@ export default React.memo(ViewerItem, (prevProps, nextProps) => {
68
148
  return false;
69
149
  }
70
150
 
71
- if (prevProps.props.sourceUrl !== nextProps.props.sourceUrl) {
151
+ if (prevProps.props.url !== nextProps.props.url) {
72
152
  return false;
73
153
  }
74
154