From b405f2aa6b2e2c42f84e93c202f808c58e71e0fa Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Thu, 16 Jul 2020 22:53:48 +0200 Subject: [PATCH] Added ability to set a custom dashboard via settings --- locales/en.json | 16 +- locales/fr.json | 16 +- .../DashboardEdit/DashboardEditAccordion.js | 72 +++++++++ .../Lists/DashboardEdit/DashboardEditItem.js | 55 +++++++ .../DashboardEdit/DashboardEditPreviewItem.js | 58 +++++++ src/managers/ServicesManager.js | 72 ++++++++- src/navigation/MainNavigator.js | 10 +- .../Other/Settings/DashboardEditScreen.js | 148 ++++++++++++++++++ .../Other/{ => Settings}/SettingsScreen.js | 14 +- src/screens/Services/ServicesScreen.js | 31 +--- 10 files changed, 448 insertions(+), 44 deletions(-) create mode 100644 src/components/Lists/DashboardEdit/DashboardEditAccordion.js create mode 100644 src/components/Lists/DashboardEdit/DashboardEditItem.js create mode 100644 src/components/Lists/DashboardEdit/DashboardEditPreviewItem.js create mode 100644 src/screens/Other/Settings/DashboardEditScreen.js rename src/screens/Other/{ => Settings}/SettingsScreen.js (93%) diff --git a/locales/en.json b/locales/en.json index cf3fa0a..2f2ed53 100644 --- a/locales/en.json +++ b/locales/en.json @@ -6,7 +6,8 @@ "categories": { "amicale": "The Amicale", "students": "Student services", - "insa": "INSA services" + "insa": "INSA services", + "special": "Proxiwash" }, "descriptions": { "clubs": "See info about your favorite club and discover new ones", @@ -23,7 +24,9 @@ "mails": "Check your INSA mails", "ent": "See your grades", "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": { "title": "So handy!", @@ -320,9 +323,16 @@ "nightModeAutoSub": "Follows the mode chosen by your system", "startScreen": "Start Screen", "startScreenSub": "Select which screen to start the app on", + "dashboard": "Dashboard", + "dashboardSub": "Edit what services to display on the dashboard", "proxiwashNotifReminder": "Machine running reminder", "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": { "title": "About", diff --git a/locales/fr.json b/locales/fr.json index 8fb7e94..aee5e37 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -6,7 +6,8 @@ "categories": { "amicale": "L' Amicale", "students": "Services étudiants", - "insa": "Services de l'INSA" + "insa": "Services de l'INSA", + "special": "Proxiwash" }, "descriptions": { "clubs": "Tous les clubs et leurs infos", @@ -23,7 +24,9 @@ "mails": "Vérifie tes mails INSA", "ent": "Retrouve tes notes", "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": { "title": "Un peu perdu ?", @@ -319,9 +322,16 @@ "nightModeAutoSub": "Suit le mode sélectionné par le système", "startScreen": "Écran de démarrage", "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", "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": { "title": "À Propos", diff --git a/src/components/Lists/DashboardEdit/DashboardEditAccordion.js b/src/components/Lists/DashboardEdit/DashboardEditAccordion.js new file mode 100644 index 0000000..1c6d738 --- /dev/null +++ b/src/components/Lists/DashboardEdit/DashboardEditAccordion.js @@ -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, + onPress: (service: ServiceItem) => void, + theme: CustomTheme, +} + +const LIST_ITEM_HEIGHT = 64; + +class DashboardEditAccordion extends React.Component { + + renderItem = ({item}: { item: ServiceItem }) => { + return ( + this.props.onPress(item)}/> + ); + } + + itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); + + render() { + const item = this.props.item; + return ( + + typeof item.image === "number" + ? + : } + > + {/*$FlowFixMe*/} + + + + ); + } +} + +export default withTheme(DashboardEditAccordion) diff --git a/src/components/Lists/DashboardEdit/DashboardEditItem.js b/src/components/Lists/DashboardEdit/DashboardEditItem.js new file mode 100644 index 0000000..1d32b9e --- /dev/null +++ b/src/components/Lists/DashboardEdit/DashboardEditItem.js @@ -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 { + + shouldComponentUpdate(nextProps: Props) { + return (nextProps.isActive !== this.props.isActive); + } + + render() { + return ( + + } + right={props => this.props.isActive + ? : null} + style={{ + height: this.props.height, + justifyContent: 'center', + paddingLeft: 30, + backgroundColor: this.props.isActive ? this.props.theme.colors.proxiwashFinishedColor : "transparent" + }} + /> + ); + } +} + +export default withTheme(DashboardEditItem); diff --git a/src/components/Lists/DashboardEdit/DashboardEditPreviewItem.js b/src/components/Lists/DashboardEdit/DashboardEditPreviewItem.js new file mode 100644 index 0000000..f4c4ce2 --- /dev/null +++ b/src/components/Lists/DashboardEdit/DashboardEditPreviewItem.js @@ -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 { + + itemSize: number; + + constructor(props: Props) { + super(props); + this.itemSize = Dimensions.get('window').width / 8; + } + + render() { + const props = this.props; + return ( + + + + + + ); + } + +} + +export default withTheme(DashboardEditPreviewItem) diff --git a/src/managers/ServicesManager.js b/src/managers/ServicesManager.js index f629fd0..046ae39 100644 --- a/src/managers/ServicesManager.js +++ b/src/managers/ServicesManager.js @@ -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 DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png"; +const AMICALE_LOGO = require("../../assets/amicale.png"); + export const SERVICES_KEY = { CLUBS: "clubs", PROFILE: "profile", @@ -51,6 +53,14 @@ export const SERVICES_KEY = { DRYERS: "dryers", } +export const SERVICES_CATEGORIES_KEY = { + AMICALE: "amicale", + STUDENTS: "students", + INSA: "insa", + SPECIAL: "special", +} + + export type ServiceItem = { key: string, title: string, @@ -60,6 +70,15 @@ export type ServiceItem = { badgeFunction?: (dashboard: fullDashboard) => number, } +export type ServiceCategory = { + key: string, + title: string, + subtitle: string, + image: string | number, + content: Array +} + + export default class ServicesManager { navigation: StackNavigationProp; @@ -69,6 +88,8 @@ export default class ServicesManager { insaDataset: Array; specialDataset: Array; + categoriesDataset: Array; + constructor(nav: StackNavigationProp) { this.navigation = nav; this.amicaleDataset = [ @@ -213,7 +234,7 @@ export default class ServicesManager { { key: SERVICES_KEY.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, onPress: () => nav.navigate("proxiwash"), badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers @@ -221,12 +242,42 @@ export default class ServicesManager { { key: SERVICES_KEY.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, onPress: () => nav.navigate("proxiwash"), 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 * @returns {[]} */ - getStrippedList(idList: Array, sourceList: Array) { + getStrippedList(idList: Array, sourceList: Array<{key: string, [key: string]: any}>) { let newArray = []; for (let i = 0; i < sourceList.length; i++) { const item = sourceList[i]; @@ -311,4 +362,17 @@ export default class ServicesManager { return this.specialDataset; } + /** + * Gets all services sorted by category + * + * @param excludedItems Ids of categories to exclude from the returned list + * @returns {Array} + */ + getCategories(excludedItems?: Array) { + if (excludedItems != null) + return this.getStrippedList(excludedItems, this.categoriesDataset) + else + return this.categoriesDataset; + } + } diff --git a/src/navigation/MainNavigator.js b/src/navigation/MainNavigator.js index 127b431..16c1e4b 100644 --- a/src/navigation/MainNavigator.js +++ b/src/navigation/MainNavigator.js @@ -1,7 +1,7 @@ // @flow 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 AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import DebugScreen from '../screens/About/DebugScreen'; @@ -26,6 +26,7 @@ import WebsiteScreen from "../screens/Services/WebsiteScreen"; import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen"; import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen"; import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen"; +import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen"; 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'), }} /> + , + currentDashboardIdList: Array, + activeItem: number, +}; + +/** + * Class defining the Settings screen. This screen shows controls to modify app preferences. + */ +class DashboardEditScreen extends React.Component { + + content: Array; + initialDashboard: Array; + initialDashboardIdList: Array; + + 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 ( + this.setState({activeItem: index})} + isActive={this.state.activeItem === index} + /> + ); + }; + + getDashboard(content: Array) { + return ( + ); + } + + renderItem = ({item}: { item: ServiceCategory }) => { + return ( + + ); + }; + + 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 ( + + + + + {this.getDashboard(this.state.currentDashboard)} + + + + {i18n.t("screens.settings.dashboardEdit.message")}} + style={{ + }} + /> + + ); + } + +} + +export default withTheme(DashboardEditScreen); diff --git a/src/screens/Other/SettingsScreen.js b/src/screens/Other/Settings/SettingsScreen.js similarity index 93% rename from src/screens/Other/SettingsScreen.js rename to src/screens/Other/Settings/SettingsScreen.js index d07240f..66d7bb4 100644 --- a/src/screens/Other/SettingsScreen.js +++ b/src/screens/Other/Settings/SettingsScreen.js @@ -2,13 +2,13 @@ import * as React from 'react'; import {ScrollView, View} from "react-native"; -import type {CustomTheme} from "../../managers/ThemeManager"; -import ThemeManager from '../../managers/ThemeManager'; +import type {CustomTheme} from "../../../managers/ThemeManager"; +import ThemeManager from '../../../managers/ThemeManager'; 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 {Appearance} from "react-native-appearance"; -import CustomSlider from "../../components/Overrides/CustomSlider"; +import CustomSlider from "../../../components/Overrides/CustomSlider"; import {StackNavigationProp} from "@react-navigation/stack"; type Props = { @@ -203,6 +203,12 @@ class SettingsScreen extends React.Component { left={props => } /> {this.getStartScreenPicker()} + this.props.navigation.navigate("dashboard-edit")} + left={props => } + /> diff --git a/src/screens/Services/ServicesScreen.js b/src/screens/Services/ServicesScreen.js index 43e9548..dd64828 100644 --- a/src/screens/Services/ServicesScreen.js +++ b/src/screens/Services/ServicesScreen.js @@ -16,7 +16,7 @@ import {StackNavigationProp} from "@react-navigation/stack"; import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import MascotPopup from "../../components/Mascot/MascotPopup"; import AsyncStorageManager from "../../managers/AsyncStorageManager"; -import ServicesManager from "../../managers/ServicesManager"; +import ServicesManager, {SERVICES_CATEGORIES_KEY} from "../../managers/ServicesManager"; type Props = { navigation: StackNavigationProp, @@ -36,14 +36,9 @@ export type listItem = { content: cardList, } -const AMICALE_LOGO = require("../../../assets/amicale.png"); class ServicesScreen extends React.Component { - amicaleDataset: cardList; - studentsDataset: cardList; - insaDataset: cardList; - finalDataset: Array state = { @@ -53,29 +48,7 @@ class ServicesScreen extends React.Component { constructor(props) { super(props); const services = new ServicesManager(props.navigation); - this.amicaleDataset = services.getAmicaleServices(); - 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 - }, - ]; + this.finalDataset = services.getCategories([SERVICES_CATEGORIES_KEY.SPECIAL]) } componentDidMount() {