Browse Source

Improve favorites handling

Arnaud Vergnet 6 months ago
parent
commit
ed4bb216a0

+ 7
- 1
locales/en.json View File

@@ -136,7 +136,13 @@
136 136
     "planex": {
137 137
       "title": "Planex",
138 138
       "noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
139
-      "favorites": "Favorites",
139
+      "favorites": {
140
+        "title": "Favorites",
141
+        "empty": {
142
+          "title": "No favorites",
143
+          "subtitle": "Clic on the star next to a group to add it to the favorites"
144
+        }
145
+      },
140 146
       "mascotDialog": {
141 147
         "title": "Don't skip class",
142 148
         "message": "Here is Planex! You can set your class and your crush's to favorites in order to find them back easily!\n\nIf you mainly use Campus for Planex, go to the settings to make the app directly start on it!",

+ 7
- 1
locales/fr.json View File

@@ -136,7 +136,13 @@
136 136
     "planex": {
137 137
       "title": "Planex",
138 138
       "noGroupSelected": "Pas de groupe sélectionné. Choisis un groupe avec le beau bouton rouge ci-dessous.",
139
-      "favorites": "Favoris",
139
+      "favorites": {
140
+        "title": "Favoris",
141
+        "empty": {
142
+          "title": "Aucun favoris",
143
+          "subtitle": "Cliquez sur l'étoile à côté d'un groupe pour l'ajouter aux favoris"
144
+        }
145
+      },
140 146
       "mascotDialog": {
141 147
         "title": "Sécher c'est mal",
142 148
         "message": "Ici c'est Planex ! Tu peux mettre en favoris ta classe et celle de ton crush pour l'espio... les retrouver facilement !\n\nSi tu utilises Campus surtout pour Planex, vas dans les paramètres pour faire démarrer l'appli direct dessus !",

+ 65
- 99
src/components/Animations/AnimatedAccordion.tsx View File

@@ -17,17 +17,14 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
21
-import { View, ViewProps, ViewStyle } from 'react-native';
22
-import { List, withTheme } from 'react-native-paper';
20
+import React, { useEffect, useRef } from 'react';
21
+import { View, ViewStyle } from 'react-native';
22
+import { List, useTheme } from 'react-native-paper';
23 23
 import Collapsible from 'react-native-collapsible';
24 24
 import * as Animatable from 'react-native-animatable';
25
-import { AnimatableProperties } from 'react-native-animatable';
26
-import { ClassicComponent } from 'react';
27 25
 import GENERAL_STYLES from '../../constants/Styles';
28 26
 
29 27
 type PropsType = {
30
-  theme: ReactNativePaper.Theme;
31 28
   title: string;
32 29
   subtitle?: string;
33 30
   style?: ViewStyle;
@@ -40,133 +37,102 @@ type PropsType = {
40 37
   }) => React.ReactNode;
41 38
   opened?: boolean;
42 39
   unmountWhenCollapsed?: boolean;
40
+  enabled?: boolean;
43 41
   children?: React.ReactNode;
44 42
 };
45 43
 
46
-type StateType = {
47
-  expanded: boolean;
48
-};
49
-
50
-class AnimatedAccordion extends React.Component<PropsType, StateType> {
51
-  viewRef:
52
-    | null
53
-    | (ClassicComponent<AnimatableProperties<ViewStyle>> & ViewProps);
54
-  handleViewRef = (
55
-    ref: ClassicComponent<AnimatableProperties<ViewStyle>> & ViewProps
56
-  ) => (this.viewRef = ref);
57
-
58
-  chevronIcon: string;
59
-
60
-  animStart: string;
44
+function AnimatedAccordion(props: PropsType) {
45
+  const theme = useTheme();
61 46
 
62
-  animEnd: string;
47
+  const [expanded, setExpanded] = React.useState(props.opened);
48
+  const lastOpenedProp = useRef(props.opened);
49
+  const chevronIcon = useRef(props.opened ? 'chevron-up' : 'chevron-down');
50
+  const animStart = useRef(props.opened ? '180deg' : '0deg');
51
+  const animEnd = useRef(props.opened ? '0deg' : '180deg');
52
+  const enabled = props.enabled !== false;
63 53
 
64
-  getAccordionAnimation():
54
+  const getAccordionAnimation = ():
65 55
     | Animatable.Animation
66 56
     | string
67
-    | Animatable.CustomAnimation {
57
+    | Animatable.CustomAnimation => {
68 58
     // I don't knwo why ts is complaining
69 59
     // The type definitions must be broken because this is a valid style and it works
70
-    if (this.state.expanded) {
60
+    if (expanded) {
71 61
       return {
72 62
         from: {
73 63
           // @ts-ignore
74
-          rotate: this.animStart,
64
+          rotate: animStart.current,
75 65
         },
76 66
         to: {
77 67
           // @ts-ignore
78
-          rotate: this.animEnd,
68
+          rotate: animEnd.current,
79 69
         },
80 70
       };
81 71
     } else {
82 72
       return {
83 73
         from: {
84 74
           // @ts-ignore
85
-          rotate: this.animEnd,
75
+          rotate: animEnd.current,
86 76
         },
87 77
         to: {
88 78
           // @ts-ignore
89
-          rotate: this.animStart,
79
+          rotate: animStart.current,
90 80
         },
91 81
       };
92 82
     }
93
-  }
94
-
95
-  constructor(props: PropsType) {
96
-    super(props);
97
-    this.chevronIcon = '';
98
-    this.animStart = '';
99
-    this.animEnd = '';
100
-    this.state = {
101
-      expanded: props.opened != null ? props.opened : false,
102
-    };
103
-    this.viewRef = null;
104
-    this.setupChevron();
105
-  }
106
-
107
-  shouldComponentUpdate(nextProps: PropsType): boolean {
108
-    const { state, props } = this;
109
-    // TODO refactor this, it shouldn't even work
110
-    if (nextProps.opened != null && nextProps.opened !== props.opened) {
111
-      state.expanded = nextProps.opened;
112
-    }
113
-    return true;
114
-  }
83
+  };
115 84
 
116
-  setupChevron() {
117
-    const { expanded } = this.state;
118
-    if (expanded) {
119
-      this.chevronIcon = 'chevron-up';
120
-      this.animStart = '180deg';
121
-      this.animEnd = '0deg';
122
-    } else {
123
-      this.chevronIcon = 'chevron-down';
124
-      this.animStart = '0deg';
125
-      this.animEnd = '180deg';
85
+  useEffect(() => {
86
+    // Force the expanded state to follow the prop when changing
87
+    if (!enabled) {
88
+      setExpanded(false);
89
+    } else if (
90
+      props.opened !== undefined &&
91
+      props.opened !== lastOpenedProp.current
92
+    ) {
93
+      setExpanded(props.opened);
126 94
     }
127
-  }
95
+  }, [enabled, props.opened]);
128 96
 
129
-  toggleAccordion = () => {
130
-    // const { expanded } = this.state;
131
-    this.setState((prevState: StateType): { expanded: boolean } => ({
132
-      expanded: !prevState.expanded,
133
-    }));
134
-  };
97
+  const toggleAccordion = () => setExpanded(!expanded);
135 98
 
136
-  render() {
137
-    const { props, state } = this;
138
-    const { colors } = props.theme;
139
-    return (
140
-      <View style={props.style}>
141
-        <List.Item
142
-          title={props.title}
143
-          description={props.subtitle}
144
-          titleStyle={state.expanded ? { color: colors.primary } : null}
145
-          onPress={this.toggleAccordion}
146
-          right={(iconProps) => (
147
-            <Animatable.View
148
-              animation={this.getAccordionAnimation()}
149
-              duration={300}
150
-              useNativeDriver={true}
151
-            >
152
-              <List.Icon
153
-                style={{ ...iconProps.style, ...GENERAL_STYLES.center }}
154
-                icon={this.chevronIcon}
155
-                color={state.expanded ? colors.primary : iconProps.color}
156
-              />
157
-            </Animatable.View>
158
-          )}
159
-          left={props.left}
160
-        />
161
-        <Collapsible collapsed={!state.expanded}>
99
+  return (
100
+    <View style={props.style}>
101
+      <List.Item
102
+        title={props.title}
103
+        description={props.subtitle}
104
+        descriptionNumberOfLines={2}
105
+        titleStyle={expanded ? { color: theme.colors.primary } : null}
106
+        onPress={enabled ? toggleAccordion : undefined}
107
+        right={
108
+          enabled
109
+            ? (iconProps) => (
110
+                <Animatable.View
111
+                  animation={getAccordionAnimation()}
112
+                  duration={300}
113
+                  useNativeDriver={true}
114
+                >
115
+                  <List.Icon
116
+                    style={{ ...iconProps.style, ...GENERAL_STYLES.center }}
117
+                    icon={chevronIcon.current}
118
+                    color={expanded ? theme.colors.primary : iconProps.color}
119
+                  />
120
+                </Animatable.View>
121
+              )
122
+            : undefined
123
+        }
124
+        left={props.left}
125
+      />
126
+      {enabled ? (
127
+        <Collapsible collapsed={!expanded}>
162 128
           {!props.unmountWhenCollapsed ||
163
-          (props.unmountWhenCollapsed && state.expanded)
129
+          (props.unmountWhenCollapsed && expanded)
164 130
             ? props.children
165 131
             : null}
166 132
         </Collapsible>
167
-      </View>
168
-    );
169
-  }
133
+      ) : null}
134
+    </View>
135
+  );
170 136
 }
171 137
 
172
-export default withTheme(AnimatedAccordion);
138
+export default AnimatedAccordion;

+ 22
- 6
src/components/Lists/PlanexGroups/GroupListAccordion.tsx View File

@@ -27,6 +27,7 @@ import type {
27 27
   PlanexGroupType,
28 28
   PlanexGroupCategoryType,
29 29
 } from '../../../screens/Planex/GroupSelectionScreen';
30
+import i18n from 'i18n-js';
30 31
 
31 32
 type PropsType = {
32 33
   item: PlanexGroupCategoryType;
@@ -101,22 +102,37 @@ class GroupListAccordion extends React.Component<PropsType> {
101 102
   render() {
102 103
     const { props } = this;
103 104
     const { item } = this.props;
105
+    var isFavorite = item.id === 0;
106
+    var isEmptyFavorite = isFavorite && props.favorites.length === 0;
104 107
     return (
105 108
       <View>
106 109
         <AnimatedAccordion
107
-          title={item.name.replace(REPLACE_REGEX, ' ')}
110
+          title={
111
+            isEmptyFavorite
112
+              ? i18n.t('screens.planex.favorites.empty.title')
113
+              : item.name.replace(REPLACE_REGEX, ' ')
114
+          }
115
+          subtitle={
116
+            isEmptyFavorite
117
+              ? i18n.t('screens.planex.favorites.empty.subtitle')
118
+              : undefined
119
+          }
108 120
           style={styles.container}
109 121
           left={(iconProps) =>
110
-            item.id === 0 ? (
122
+            isFavorite ? (
111 123
               <List.Icon
112 124
                 style={iconProps.style}
113
-                icon="star"
125
+                icon={'star'}
114 126
                 color={props.theme.colors.tetrisScore}
115 127
               />
116
-            ) : null
128
+            ) : undefined
129
+          }
130
+          unmountWhenCollapsed={!isFavorite} // Only render list if expanded for increased performance
131
+          opened={
132
+            props.currentSearchString.length > 0 ||
133
+            (isFavorite && !isEmptyFavorite)
117 134
           }
118
-          unmountWhenCollapsed={item.id !== 0} // Only render list if expanded for increased performance
119
-          opened={props.currentSearchString.length > 0}
135
+          enabled={!isEmptyFavorite}
120 136
         >
121 137
           <FlatList
122 138
             data={this.getData()}

+ 1
- 1
src/components/Lists/PlanexGroups/GroupListItem.tsx View File

@@ -106,7 +106,7 @@ class GroupListItem extends React.Component<PropsType> {
106 106
           <List.Icon
107 107
             color={iconProps.color}
108 108
             style={iconProps.style}
109
-            icon="chevron-right"
109
+            icon={'chevron-right'}
110 110
           />
111 111
         )}
112 112
         right={(iconProps) => (

+ 1
- 1
src/screens/Planex/GroupSelectionScreen.tsx View File

@@ -220,7 +220,7 @@ function GroupSelectionScreen() {
220 220
       );
221 221
       data.sort(sortName);
222 222
       data.unshift({
223
-        name: i18n.t('screens.planex.favorites'),
223
+        name: i18n.t('screens.planex.favorites.title'),
224 224
         id: 0,
225 225
         content: favoriteGroups,
226 226
       });

Loading…
Cancel
Save