Added ability to set a custom dashboard via settings

This commit is contained in:
Arnaud Vergnet 2020-07-16 22:53:48 +02:00
parent 2022b738f5
commit b405f2aa6b
10 changed files with 448 additions and 44 deletions

View file

@ -6,7 +6,8 @@
"categories": { "categories": {
"amicale": "The Amicale", "amicale": "The Amicale",
"students": "Student services", "students": "Student services",
"insa": "INSA services" "insa": "INSA services",
"special": "Proxiwash"
}, },
"descriptions": { "descriptions": {
"clubs": "See info about your favorite club and discover new ones", "clubs": "See info about your favorite club and discover new ones",
@ -23,7 +24,9 @@
"mails": "Check your INSA mails", "mails": "Check your INSA mails",
"ent": "See your grades", "ent": "See your grades",
"insaAccount": "See your information and change your password", "insaAccount": "See your information and change your password",
"equipment": "Book a BBQ or other equipment" "equipment": "Book a BBQ or other equipment",
"washers": "Number of available washers",
"dryers": "Number of available dryers"
}, },
"mascotDialog": { "mascotDialog": {
"title": "So handy!", "title": "So handy!",
@ -320,9 +323,16 @@
"nightModeAutoSub": "Follows the mode chosen by your system", "nightModeAutoSub": "Follows the mode chosen by your system",
"startScreen": "Start Screen", "startScreen": "Start Screen",
"startScreenSub": "Select which screen to start the app on", "startScreenSub": "Select which screen to start the app on",
"dashboard": "Dashboard",
"dashboardSub": "Edit what services to display on the dashboard",
"proxiwashNotifReminder": "Machine running reminder", "proxiwashNotifReminder": "Machine running reminder",
"proxiwashNotifReminderSub": "How many minutes before", "proxiwashNotifReminderSub": "How many minutes before",
"information": "Information" "information": "Information",
"dashboardEdit": {
"title": "Edit dashboard",
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list bellow.",
"undo": "Undo changes"
}
}, },
"about": { "about": {
"title": "About", "title": "About",

View file

@ -6,7 +6,8 @@
"categories": { "categories": {
"amicale": "L' Amicale", "amicale": "L' Amicale",
"students": "Services étudiants", "students": "Services étudiants",
"insa": "Services de l'INSA" "insa": "Services de l'INSA",
"special": "Proxiwash"
}, },
"descriptions": { "descriptions": {
"clubs": "Tous les clubs et leurs infos", "clubs": "Tous les clubs et leurs infos",
@ -23,7 +24,9 @@
"mails": "Vérifie tes mails INSA", "mails": "Vérifie tes mails INSA",
"ent": "Retrouve tes notes", "ent": "Retrouve tes notes",
"insaAccount": "Accède à tes infos INSA et modifie ton mot de passe", "insaAccount": "Accède à tes infos INSA et modifie ton mot de passe",
"equipment": "Réserve un BBQ ou autre matériel" "equipment": "Réserve un BBQ ou autre matériel",
"washers": "Nombre de lave-Linges disponibles",
"dryers": "Nombre de sèche-Linges disponibles"
}, },
"mascotDialog": { "mascotDialog": {
"title": "Un peu perdu ?", "title": "Un peu perdu ?",
@ -319,9 +322,16 @@
"nightModeAutoSub": "Suit le mode sélectionné par le système", "nightModeAutoSub": "Suit le mode sélectionné par le système",
"startScreen": "Écran de démarrage", "startScreen": "Écran de démarrage",
"startScreenSub": "Choisis l'écran sur lequel démarre Campus", "startScreenSub": "Choisis l'écran sur lequel démarre Campus",
"dashboard": "Dashboard",
"dashboardSub": "Choisis les services à afficher sur la dashboard",
"proxiwashNotifReminder": "Rappel de machine en cours", "proxiwashNotifReminder": "Rappel de machine en cours",
"proxiwashNotifReminderSub": "Combien de minutes avant", "proxiwashNotifReminderSub": "Combien de minutes avant",
"information": "Informations" "information": "Informations",
"dashboardEdit": {
"title": "Modifier la dashboard",
"message": "Les 5 icones ci-dessus représentent ta dashboard.\nTu peux remplacer un de ses services en cliquant dessus, puis en sélectionnant le nouveau service de ton choix dans la liste ci-dessous.",
"undo": "Annuler les changements"
}
}, },
"about": { "about": {
"title": "À Propos", "title": "À Propos",

View file

@ -0,0 +1,72 @@
// @flow
import * as React from 'react';
import {withTheme} from 'react-native-paper';
import {FlatList, Image, View} from "react-native";
import DashboardEditItem from "./DashboardEditItem";
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
import type {ServiceCategory, ServiceItem} from "../../../managers/ServicesManager";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import type {CustomTheme} from "../../../managers/ThemeManager";
type Props = {
item: ServiceCategory,
activeDashboard: Array<string>,
onPress: (service: ServiceItem) => void,
theme: CustomTheme,
}
const LIST_ITEM_HEIGHT = 64;
class DashboardEditAccordion extends React.Component<Props> {
renderItem = ({item}: { item: ServiceItem }) => {
return (
<DashboardEditItem
height={LIST_ITEM_HEIGHT}
item={item}
isActive={this.props.activeDashboard.includes(item.key)}
onPress={() => this.props.onPress(item)}/>
);
}
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
render() {
const item = this.props.item;
return (
<View>
<AnimatedAccordion
title={item.title}
left={props => typeof item.image === "number"
? <Image
{...props}
source={item.image}
style={{
width: 40,
height: 40
}}
/>
: <MaterialCommunityIcons
//$FlowFixMe
name={item.image}
color={this.props.theme.colors.primary}
size={40}/>}
>
{/*$FlowFixMe*/}
<FlatList
data={item.content}
extraData={this.props.activeDashboard.toString()}
renderItem={this.renderItem}
listKey={item.key}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getItemLayout={this.itemLayout}
removeClippedSubviews={true}
/>
</AnimatedAccordion>
</View>
);
}
}
export default withTheme(DashboardEditAccordion)

View file

@ -0,0 +1,55 @@
// @flow
import * as React from 'react';
import {Image} from "react-native";
import {List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ServiceItem} from "../../../managers/ServicesManager";
type Props = {
item: ServiceItem,
isActive: boolean,
height: number,
onPress: () => void,
theme: CustomTheme,
}
class DashboardEditItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return (nextProps.isActive !== this.props.isActive);
}
render() {
return (
<List.Item
title={this.props.item.title}
description={this.props.item.subtitle}
onPress={this.props.isActive ? null : this.props.onPress}
left={props =>
<Image
{...props}
source={{uri: this.props.item.image}}
style={{
width: 40,
height: 40
}}
/>}
right={props => this.props.isActive
? <List.Icon
{...props}
icon={"check"}
color={this.props.theme.colors.success}
/> : null}
style={{
height: this.props.height,
justifyContent: 'center',
paddingLeft: 30,
backgroundColor: this.props.isActive ? this.props.theme.colors.proxiwashFinishedColor : "transparent"
}}
/>
);
}
}
export default withTheme(DashboardEditItem);

View file

@ -0,0 +1,58 @@
// @flow
import * as React from 'react';
import {TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from "react-native";
import type {CustomTheme} from "../../../managers/ThemeManager";
type Props = {
image: string,
isActive: boolean,
onPress: () => void,
theme: CustomTheme,
};
/**
* Component used to render a small dashboard item
*/
class DashboardEditPreviewItem extends React.Component<Props> {
itemSize: number;
constructor(props: Props) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
render() {
const props = this.props;
return (
<TouchableRipple
onPress={this.props.onPress}
borderless={true}
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: this.props.isActive ? this.props.theme.colors.textDisabled : "transparent",
borderRadius: 5
}}
>
<View style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: "100%",
height: "100%",
}}
/>
</View>
</TouchableRipple>
);
}
}
export default withTheme(DashboardEditPreviewItem)

View file

@ -31,6 +31,8 @@ const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account
const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png"; const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png";
const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png"; const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png";
const AMICALE_LOGO = require("../../assets/amicale.png");
export const SERVICES_KEY = { export const SERVICES_KEY = {
CLUBS: "clubs", CLUBS: "clubs",
PROFILE: "profile", PROFILE: "profile",
@ -51,6 +53,14 @@ export const SERVICES_KEY = {
DRYERS: "dryers", DRYERS: "dryers",
} }
export const SERVICES_CATEGORIES_KEY = {
AMICALE: "amicale",
STUDENTS: "students",
INSA: "insa",
SPECIAL: "special",
}
export type ServiceItem = { export type ServiceItem = {
key: string, key: string,
title: string, title: string,
@ -60,6 +70,15 @@ export type ServiceItem = {
badgeFunction?: (dashboard: fullDashboard) => number, badgeFunction?: (dashboard: fullDashboard) => number,
} }
export type ServiceCategory = {
key: string,
title: string,
subtitle: string,
image: string | number,
content: Array<ServiceItem>
}
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp; navigation: StackNavigationProp;
@ -69,6 +88,8 @@ export default class ServicesManager {
insaDataset: Array<ServiceItem>; insaDataset: Array<ServiceItem>;
specialDataset: Array<ServiceItem>; specialDataset: Array<ServiceItem>;
categoriesDataset: Array<ServiceCategory>;
constructor(nav: StackNavigationProp) { constructor(nav: StackNavigationProp) {
this.navigation = nav; this.navigation = nav;
this.amicaleDataset = [ this.amicaleDataset = [
@ -213,7 +234,7 @@ export default class ServicesManager {
{ {
key: SERVICES_KEY.WASHERS, key: SERVICES_KEY.WASHERS,
title: i18n.t('screens.proxiwash.washers'), title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.proxiwash.title'), // TODO add description subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE, image: WASHER_IMAGE,
onPress: () => nav.navigate("proxiwash"), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
@ -221,12 +242,42 @@ export default class ServicesManager {
{ {
key: SERVICES_KEY.DRYERS, key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'), title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.proxiwash.title'), // TODO add description subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE, image: DRYER_IMAGE,
onPress: () => nav.navigate("proxiwash"), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
} }
] ];
this.categoriesDataset = [
{
key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t("screens.services.categories.amicale"),
subtitle: i18n.t("screens.services.more"),
image: AMICALE_LOGO,
content: this.amicaleDataset
},
{
key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t("screens.services.categories.students"),
subtitle: i18n.t("screens.services.more"),
image: 'account-group',
content: this.studentsDataset
},
{
key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t("screens.services.categories.insa"),
subtitle: i18n.t("screens.services.more"),
image: 'school',
content: this.insaDataset
},
{
key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t("screens.services.categories.special"),
subtitle: i18n.t("screens.services.categories.special"),
image: 'star',
content: this.specialDataset
},
];
} }
/** /**
@ -249,7 +300,7 @@ export default class ServicesManager {
* @param sourceList The item list to use as source * @param sourceList The item list to use as source
* @returns {[]} * @returns {[]}
*/ */
getStrippedList(idList: Array<string>, sourceList: Array<ServiceItem>) { getStrippedList(idList: Array<string>, sourceList: Array<{key: string, [key: string]: any}>) {
let newArray = []; let newArray = [];
for (let i = 0; i < sourceList.length; i++) { for (let i = 0; i < sourceList.length; i++) {
const item = sourceList[i]; const item = sourceList[i];
@ -311,4 +362,17 @@ export default class ServicesManager {
return this.specialDataset; return this.specialDataset;
} }
/**
* Gets all services sorted by category
*
* @param excludedItems Ids of categories to exclude from the returned list
* @returns {Array<ServiceCategory>}
*/
getCategories(excludedItems?: Array<string>) {
if (excludedItems != null)
return this.getStrippedList(excludedItems, this.categoriesDataset)
else
return this.categoriesDataset;
}
} }

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import SettingsScreen from '../screens/Other/SettingsScreen'; import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
import DebugScreen from '../screens/About/DebugScreen'; import DebugScreen from '../screens/About/DebugScreen';
@ -26,6 +26,7 @@ import WebsiteScreen from "../screens/Services/WebsiteScreen";
import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen"; import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen"; import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen"; import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS; const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
@ -62,6 +63,13 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
title: i18n.t('screens.settings.title'), title: i18n.t('screens.settings.title'),
}} }}
/> />
<MainStack.Screen
name="dashboard-edit"
component={DashboardEditScreen}
options={{
title: i18n.t('screens.settings.dashboardEdit.title'),
}}
/>
<MainStack.Screen <MainStack.Screen
name="about" name="about"
component={AboutScreen} component={AboutScreen}

View file

@ -0,0 +1,148 @@
// @flow
import * as React from 'react';
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import {Button, Paragraph, withTheme} from "react-native-paper";
import type {ServiceCategory, ServiceItem} from "../../../managers/ServicesManager";
import DashboardManager from "../../../managers/DashboardManager";
import DashboardItem from "../../../components/Home/EventDashboardItem";
import {FlatList} from "react-native";
import {View} from "react-native-animatable";
import DashboardEditAccordion from "../../../components/Lists/DashboardEdit/DashboardEditAccordion";
import DashboardEditPreviewItem from "../../../components/Lists/DashboardEdit/DashboardEditPreviewItem";
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
import i18n from "i18n-js";
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
};
type State = {
currentDashboard: Array<ServiceItem>,
currentDashboardIdList: Array<string>,
activeItem: number,
};
/**
* Class defining the Settings screen. This screen shows controls to modify app preferences.
*/
class DashboardEditScreen extends React.Component<Props, State> {
content: Array<ServiceCategory>;
initialDashboard: Array<ServiceItem>;
initialDashboardIdList: Array<string>;
constructor(props: Props) {
super(props);
let dashboardManager = new DashboardManager(this.props.navigation);
this.initialDashboardIdList = JSON.parse(AsyncStorageManager.getInstance().preferences.dashboardItems.current);
this.initialDashboard = dashboardManager.getCurrentDashboard();
this.state = {
currentDashboard: [...this.initialDashboard],
currentDashboardIdList: [...this.initialDashboardIdList],
activeItem: 0,
}
this.content = dashboardManager.getCategories();
}
dashboardRowRenderItem = ({item, index}: { item: DashboardItem, index: number }) => {
return (
<DashboardEditPreviewItem
image={item.image}
onPress={() => this.setState({activeItem: index})}
isActive={this.state.activeItem === index}
/>
);
};
getDashboard(content: Array<DashboardItem>) {
return (
<FlatList
data={content}
extraData={this.state}
renderItem={this.dashboardRowRenderItem}
horizontal={true}
contentContainerStyle={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 5,
}}
/>);
}
renderItem = ({item}: { item: ServiceCategory }) => {
return (
<DashboardEditAccordion
item={item}
onPress={this.updateDashboard}
activeDashboard={this.state.currentDashboardIdList}
/>
);
};
updateDashboard = (service: ServiceItem) => {
let currentDashboard = this.state.currentDashboard;
let currentDashboardIdList = this.state.currentDashboardIdList;
currentDashboard[this.state.activeItem] = service;
currentDashboardIdList[this.state.activeItem] = service.key;
this.setState({
currentDashboard: currentDashboard,
currentDashboardIdList: currentDashboardIdList,
});
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.dashboardItems.key,
JSON.stringify(currentDashboardIdList)
);
}
undoDashboard= () => {
this.setState({
currentDashboard: [...this.initialDashboard],
currentDashboardIdList: [...this.initialDashboardIdList]
});
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.dashboardItems.key,
JSON.stringify(this.initialDashboardIdList)
);
}
render() {
return (
<View style={{flex: 1}}>
<View style={{
padding: 5
}}>
<Button
mode={"contained"}
onPress={this.undoDashboard}
style={{
marginLeft: "auto",
marginRight: "auto",
}}
>
{i18n.t("screens.settings.dashboardEdit.undo")}
</Button>
<View style={{
height: 50
}}>
{this.getDashboard(this.state.currentDashboard)}
</View>
</View>
<FlatList
data={this.content}
renderItem={this.renderItem}
ListHeaderComponent={<Paragraph>{i18n.t("screens.settings.dashboardEdit.message")}</Paragraph>}
style={{
}}
/>
</View>
);
}
}
export default withTheme(DashboardEditScreen);

View file

@ -2,13 +2,13 @@
import * as React from 'react'; import * as React from 'react';
import {ScrollView, View} from "react-native"; import {ScrollView, View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomTheme} from "../../../managers/ThemeManager";
import ThemeManager from '../../managers/ThemeManager'; import ThemeManager from '../../../managers/ThemeManager';
import i18n from "i18n-js"; import i18n from "i18n-js";
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AsyncStorageManager from "../../../managers/AsyncStorageManager";
import {Card, List, Switch, ToggleButton, withTheme} from 'react-native-paper'; import {Card, List, Switch, ToggleButton, withTheme} from 'react-native-paper';
import {Appearance} from "react-native-appearance"; import {Appearance} from "react-native-appearance";
import CustomSlider from "../../components/Overrides/CustomSlider"; import CustomSlider from "../../../components/Overrides/CustomSlider";
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
@ -203,6 +203,12 @@ class SettingsScreen extends React.Component<Props, State> {
left={props => <List.Icon {...props} icon="power"/>} left={props => <List.Icon {...props} icon="power"/>}
/> />
{this.getStartScreenPicker()} {this.getStartScreenPicker()}
<List.Item
title={i18n.t('screens.settings.dashboard')}
description={i18n.t('screens.settings.dashboardSub')}
onPress={() => this.props.navigation.navigate("dashboard-edit")}
left={props => <List.Icon {...props} icon="view-dashboard"/>}
/>
</List.Section> </List.Section>
</Card> </Card>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>

View file

@ -16,7 +16,7 @@ import {StackNavigationProp} from "@react-navigation/stack";
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import MascotPopup from "../../components/Mascot/MascotPopup"; import MascotPopup from "../../components/Mascot/MascotPopup";
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import ServicesManager from "../../managers/ServicesManager"; import ServicesManager, {SERVICES_CATEGORIES_KEY} from "../../managers/ServicesManager";
type Props = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
@ -36,14 +36,9 @@ export type listItem = {
content: cardList, content: cardList,
} }
const AMICALE_LOGO = require("../../../assets/amicale.png");
class ServicesScreen extends React.Component<Props, State> { class ServicesScreen extends React.Component<Props, State> {
amicaleDataset: cardList;
studentsDataset: cardList;
insaDataset: cardList;
finalDataset: Array<listItem> finalDataset: Array<listItem>
state = { state = {
@ -53,29 +48,7 @@ class ServicesScreen extends React.Component<Props, State> {
constructor(props) { constructor(props) {
super(props); super(props);
const services = new ServicesManager(props.navigation); const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices(); this.finalDataset = services.getCategories([SERVICES_CATEGORIES_KEY.SPECIAL])
this.studentsDataset = services.getStudentServices();
this.insaDataset = services.getINSAServices();
this.finalDataset = [
{
title: i18n.t("screens.services.categories.amicale"),
description: i18n.t("screens.services.more"),
image: AMICALE_LOGO,
content: this.amicaleDataset
},
{
title: i18n.t("screens.services.categories.students"),
description: i18n.t("screens.services.more"),
image: 'account-group',
content: this.studentsDataset
},
{
title: i18n.t("screens.services.categories.insa"),
description: i18n.t("screens.services.more"),
image: 'school',
content: this.insaDataset
},
];
} }
componentDidMount() { componentDidMount() {