Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProxiwashScreen.tsx 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import * as React from 'react';
  20. import {
  21. Alert,
  22. SectionListData,
  23. SectionListRenderItemInfo,
  24. StyleSheet,
  25. View,
  26. } from 'react-native';
  27. import i18n from 'i18n-js';
  28. import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper';
  29. import { StackNavigationProp } from '@react-navigation/stack';
  30. import { Modalize } from 'react-native-modalize';
  31. import WebSectionList from '../../components/Screens/WebSectionList';
  32. import * as Notifications from '../../utils/Notifications';
  33. import AsyncStorageManager from '../../managers/AsyncStorageManager';
  34. import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem';
  35. import ProxiwashConstants, {
  36. MachineStates,
  37. } from '../../constants/ProxiwashConstants';
  38. import CustomModal from '../../components/Overrides/CustomModal';
  39. import AprilFoolsManager from '../../managers/AprilFoolsManager';
  40. import MaterialHeaderButtons, {
  41. Item,
  42. } from '../../components/Overrides/CustomHeaderButton';
  43. import ProxiwashSectionHeader from '../../components/Lists/Proxiwash/ProxiwashSectionHeader';
  44. import {
  45. getCleanedMachineWatched,
  46. getMachineEndDate,
  47. isMachineWatched,
  48. } from '../../utils/Proxiwash';
  49. import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
  50. import MascotPopup from '../../components/Mascot/MascotPopup';
  51. import type { SectionListDataType } from '../../components/Screens/WebSectionList';
  52. import type { LaundromatType } from './ProxiwashAboutScreen';
  53. import GENERAL_STYLES from '../../constants/Styles';
  54. import { readData } from '../../utils/WebData';
  55. const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
  56. const LIST_ITEM_HEIGHT = 64;
  57. export type ProxiwashMachineType = {
  58. number: string;
  59. state: MachineStates;
  60. maxWeight: number;
  61. startTime: string;
  62. endTime: string;
  63. donePercent: string;
  64. remainingTime: string;
  65. program: string;
  66. };
  67. type PropsType = {
  68. navigation: StackNavigationProp<any>;
  69. theme: ReactNativePaper.Theme;
  70. };
  71. type StateType = {
  72. modalCurrentDisplayItem: React.ReactNode;
  73. machinesWatched: Array<ProxiwashMachineType>;
  74. selectedWash: string;
  75. };
  76. type FetchedDataType = {
  77. dryers: Array<ProxiwashMachineType>;
  78. washers: Array<ProxiwashMachineType>;
  79. };
  80. const styles = StyleSheet.create({
  81. modalContainer: {
  82. flex: 1,
  83. padding: 20,
  84. },
  85. icon: {
  86. backgroundColor: 'transparent',
  87. },
  88. container: {
  89. position: 'absolute',
  90. width: '100%',
  91. height: '100%',
  92. },
  93. });
  94. /**
  95. * Class defining the app's proxiwash screen. This screen shows information about washing machines and
  96. * dryers, taken from a scrapper reading proxiwash website
  97. */
  98. class ProxiwashScreen extends React.Component<PropsType, StateType> {
  99. /**
  100. * Shows a warning telling the user notifications are disabled for the app
  101. */
  102. static showNotificationsDisabledWarning() {
  103. Alert.alert(
  104. i18n.t('screens.proxiwash.modal.notificationErrorTitle'),
  105. i18n.t('screens.proxiwash.modal.notificationErrorDescription')
  106. );
  107. }
  108. modalStateStrings: { [key in MachineStates]: string } = {
  109. [MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.modal.ready'),
  110. [MachineStates.RUNNING]: i18n.t('screens.proxiwash.modal.running'),
  111. [MachineStates.RUNNING_NOT_STARTED]: i18n.t(
  112. 'screens.proxiwash.modal.runningNotStarted'
  113. ),
  114. [MachineStates.FINISHED]: i18n.t('screens.proxiwash.modal.finished'),
  115. [MachineStates.UNAVAILABLE]: i18n.t('screens.proxiwash.modal.broken'),
  116. [MachineStates.ERROR]: i18n.t('screens.proxiwash.modal.error'),
  117. [MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.modal.unknown'),
  118. };
  119. modalRef: null | Modalize;
  120. fetchedData: {
  121. dryers: Array<ProxiwashMachineType>;
  122. washers: Array<ProxiwashMachineType>;
  123. };
  124. /**
  125. * Creates machine state parameters using current theme and translations
  126. */
  127. constructor(props: PropsType) {
  128. super(props);
  129. this.modalRef = null;
  130. this.fetchedData = { dryers: [], washers: [] };
  131. this.state = {
  132. modalCurrentDisplayItem: null,
  133. machinesWatched: AsyncStorageManager.getObject(
  134. AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key
  135. ),
  136. selectedWash: AsyncStorageManager.getString(
  137. AsyncStorageManager.PREFERENCES.selectedWash.key
  138. ),
  139. };
  140. }
  141. /**
  142. * Setup notification channel for android and add listeners to detect notifications fired
  143. */
  144. componentDidMount() {
  145. const { navigation } = this.props;
  146. navigation.setOptions({
  147. headerRight: () => (
  148. <MaterialHeaderButtons>
  149. <Item
  150. title="switch"
  151. iconName="swap-horizontal"
  152. onPress={(): void => navigation.navigate('settings')}
  153. />
  154. <Item
  155. title="information"
  156. iconName="information"
  157. onPress={this.onAboutPress}
  158. />
  159. </MaterialHeaderButtons>
  160. ),
  161. });
  162. navigation.addListener('focus', this.onScreenFocus);
  163. }
  164. onScreenFocus = () => {
  165. const { state } = this;
  166. const selected = AsyncStorageManager.getString(
  167. AsyncStorageManager.PREFERENCES.selectedWash.key
  168. );
  169. if (selected !== state.selectedWash) {
  170. this.setState({
  171. selectedWash: selected,
  172. });
  173. }
  174. };
  175. /**
  176. * Callback used when pressing the about button.
  177. * This will open the ProxiwashAboutScreen.
  178. */
  179. onAboutPress = () => {
  180. const { navigation } = this.props;
  181. navigation.navigate('proxiwash-about');
  182. };
  183. /**
  184. * Callback used when the user clicks on enable notifications for a machine
  185. *
  186. * @param machine The machine to set notifications for
  187. */
  188. onSetupNotificationsPress(machine: ProxiwashMachineType) {
  189. if (this.modalRef) {
  190. this.modalRef.close();
  191. }
  192. this.setupNotifications(machine);
  193. }
  194. /**
  195. * Callback used when receiving modal ref
  196. *
  197. * @param ref
  198. */
  199. onModalRef = (ref: Modalize) => {
  200. this.modalRef = ref;
  201. };
  202. /**
  203. * Generates the modal content.
  204. * This shows information for the given machine.
  205. *
  206. * @param title The title to use
  207. * @param item The item to display information for in the modal
  208. * @param isDryer True if the given item is a dryer
  209. * @return {*}
  210. */
  211. getModalContent(title: string, item: ProxiwashMachineType, isDryer: boolean) {
  212. const { props, state } = this;
  213. let button: { text: string; icon: string; onPress: () => void } | undefined;
  214. let message = this.modalStateStrings[item.state];
  215. const onPress = () => this.onSetupNotificationsPress(item);
  216. if (item.state === MachineStates.RUNNING) {
  217. let remainingTime = parseInt(item.remainingTime, 10);
  218. if (remainingTime < 0) {
  219. remainingTime = 0;
  220. }
  221. button = {
  222. text: isMachineWatched(item, state.machinesWatched)
  223. ? i18n.t('screens.proxiwash.modal.disableNotifications')
  224. : i18n.t('screens.proxiwash.modal.enableNotifications'),
  225. icon: '',
  226. onPress: onPress,
  227. };
  228. message = i18n.t('screens.proxiwash.modal.running', {
  229. start: item.startTime,
  230. end: item.endTime,
  231. remaining: remainingTime,
  232. program: item.program,
  233. });
  234. }
  235. return (
  236. <View style={styles.modalContainer}>
  237. <Card.Title
  238. title={title}
  239. left={() => (
  240. <Avatar.Icon
  241. icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
  242. color={props.theme.colors.text}
  243. style={styles.icon}
  244. />
  245. )}
  246. />
  247. <Card.Content>
  248. <Text>{message}</Text>
  249. </Card.Content>
  250. {button ? (
  251. <Card.Actions>
  252. <Button
  253. icon={button.icon}
  254. mode="contained"
  255. onPress={button.onPress}
  256. style={GENERAL_STYLES.centerHorizontal}
  257. >
  258. {button.text}
  259. </Button>
  260. </Card.Actions>
  261. ) : null}
  262. </View>
  263. );
  264. }
  265. /**
  266. * Gets the section render item
  267. *
  268. * @param section The section to render
  269. * @return {*}
  270. */
  271. getRenderSectionHeader = ({
  272. section,
  273. }: {
  274. section: SectionListData<ProxiwashMachineType>;
  275. }) => {
  276. const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
  277. const nbAvailable = this.getMachineAvailableNumber(isDryer);
  278. return (
  279. <ProxiwashSectionHeader
  280. title={section.title}
  281. nbAvailable={nbAvailable}
  282. isDryer={isDryer}
  283. />
  284. );
  285. };
  286. /**
  287. * Gets the list item to be rendered
  288. *
  289. * @param item The object containing the item's FetchedData
  290. * @param section The object describing the current SectionList section
  291. * @returns {React.Node}
  292. */
  293. getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => {
  294. const { machinesWatched } = this.state;
  295. const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
  296. return (
  297. <ProxiwashListItem
  298. item={data.item}
  299. onPress={this.showModal}
  300. isWatched={isMachineWatched(data.item, machinesWatched)}
  301. isDryer={isDryer}
  302. height={LIST_ITEM_HEIGHT}
  303. />
  304. );
  305. };
  306. /**
  307. * Extracts the key for the given item
  308. *
  309. * @param item The item to extract the key from
  310. * @return {*} The extracted key
  311. */
  312. getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
  313. /**
  314. * Setups notifications for the machine with the given ID.
  315. * One notification will be sent at the end of the program.
  316. * Another will be send a few minutes before the end, based on the value of reminderNotifTime
  317. *
  318. * @param machine The machine to watch
  319. */
  320. setupNotifications(machine: ProxiwashMachineType) {
  321. const { machinesWatched } = this.state;
  322. if (!isMachineWatched(machine, machinesWatched)) {
  323. Notifications.setupMachineNotification(
  324. machine.number,
  325. true,
  326. getMachineEndDate(machine)
  327. )
  328. .then(() => {
  329. this.saveNotificationToState(machine);
  330. })
  331. .catch(() => {
  332. ProxiwashScreen.showNotificationsDisabledWarning();
  333. });
  334. } else {
  335. Notifications.setupMachineNotification(machine.number, false, null).then(
  336. () => {
  337. this.removeNotificationFromState(machine);
  338. }
  339. );
  340. }
  341. }
  342. /**
  343. * Gets the number of machines available
  344. *
  345. * @param isDryer True if we are only checking for dryer, false for washers
  346. * @return {number} The number of machines available
  347. */
  348. getMachineAvailableNumber(isDryer: boolean): number {
  349. let data;
  350. if (isDryer) {
  351. data = this.fetchedData.dryers;
  352. } else {
  353. data = this.fetchedData.washers;
  354. }
  355. let count = 0;
  356. data.forEach((machine: ProxiwashMachineType) => {
  357. if (machine.state === MachineStates.AVAILABLE) {
  358. count += 1;
  359. }
  360. });
  361. return count;
  362. }
  363. /**
  364. * Creates the dataset to be used by the FlatList
  365. *
  366. * @param fetchedData
  367. * @return {*}
  368. */
  369. createDataset = (
  370. fetchedData: FetchedDataType | undefined
  371. ): SectionListDataType<ProxiwashMachineType> => {
  372. const { state } = this;
  373. if (fetchedData) {
  374. let data = fetchedData;
  375. if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
  376. data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
  377. AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
  378. AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
  379. }
  380. this.fetchedData = data;
  381. // TODO dirty, should be refactored
  382. this.state.machinesWatched = getCleanedMachineWatched(
  383. state.machinesWatched,
  384. [...data.dryers, ...data.washers]
  385. );
  386. return [
  387. {
  388. title: i18n.t('screens.proxiwash.dryers'),
  389. icon: 'tumble-dryer',
  390. data: data.dryers === undefined ? [] : data.dryers,
  391. keyExtractor: this.getKeyExtractor,
  392. },
  393. {
  394. title: i18n.t('screens.proxiwash.washers'),
  395. icon: 'washing-machine',
  396. data: data.washers === undefined ? [] : data.washers,
  397. keyExtractor: this.getKeyExtractor,
  398. },
  399. ];
  400. } else {
  401. return [];
  402. }
  403. };
  404. /**
  405. * Callback used when the user clicks on the navigate to settings button.
  406. * This will hide the banner and open the SettingsScreen
  407. */
  408. onGoToSettings = () => {
  409. const { navigation } = this.props;
  410. navigation.navigate('settings');
  411. };
  412. /**
  413. * Shows a modal for the given item
  414. *
  415. * @param title The title to use
  416. * @param item The item to display information for in the modal
  417. * @param isDryer True if the given item is a dryer
  418. */
  419. showModal = (title: string, item: ProxiwashMachineType, isDryer: boolean) => {
  420. this.setState({
  421. modalCurrentDisplayItem: this.getModalContent(title, item, isDryer),
  422. });
  423. if (this.modalRef) {
  424. this.modalRef.open();
  425. }
  426. };
  427. /**
  428. * Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
  429. *
  430. * @param machine
  431. */
  432. saveNotificationToState(machine: ProxiwashMachineType) {
  433. const { machinesWatched } = this.state;
  434. const data = machinesWatched;
  435. data.push(machine);
  436. this.saveNewWatchedList(data);
  437. }
  438. /**
  439. * Removes the given index from the watchlist array and saves it to preferences
  440. *
  441. * @param selectedMachine
  442. */
  443. removeNotificationFromState(selectedMachine: ProxiwashMachineType) {
  444. const { machinesWatched } = this.state;
  445. const newList = [...machinesWatched];
  446. machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => {
  447. if (
  448. machine.number === selectedMachine.number &&
  449. machine.endTime === selectedMachine.endTime
  450. ) {
  451. newList.splice(index, 1);
  452. }
  453. });
  454. this.saveNewWatchedList(newList);
  455. }
  456. saveNewWatchedList(list: Array<ProxiwashMachineType>) {
  457. this.setState({ machinesWatched: list });
  458. AsyncStorageManager.set(
  459. AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
  460. list
  461. );
  462. }
  463. render() {
  464. const { state } = this;
  465. let data: LaundromatType;
  466. switch (state.selectedWash) {
  467. case 'tripodeB':
  468. data = ProxiwashConstants.tripodeB;
  469. break;
  470. default:
  471. data = ProxiwashConstants.washinsa;
  472. }
  473. return (
  474. <View style={GENERAL_STYLES.flex}>
  475. <View style={styles.container}>
  476. <WebSectionList
  477. request={() => readData<FetchedDataType>(data.url)}
  478. createDataset={this.createDataset}
  479. renderItem={this.getRenderItem}
  480. renderSectionHeader={this.getRenderSectionHeader}
  481. autoRefreshTime={REFRESH_TIME}
  482. refreshOnFocus={true}
  483. updateData={state.machinesWatched.length}
  484. />
  485. </View>
  486. <MascotPopup
  487. prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowMascot.key}
  488. title={i18n.t('screens.proxiwash.mascotDialog.title')}
  489. message={i18n.t('screens.proxiwash.mascotDialog.message')}
  490. icon="information"
  491. buttons={{
  492. action: {
  493. message: i18n.t('screens.proxiwash.mascotDialog.ok'),
  494. icon: 'cog',
  495. onPress: this.onGoToSettings,
  496. },
  497. cancel: {
  498. message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
  499. icon: 'close',
  500. },
  501. }}
  502. emotion={MASCOT_STYLE.NORMAL}
  503. />
  504. <CustomModal onRef={this.onModalRef}>
  505. {state.modalCurrentDisplayItem}
  506. </CustomModal>
  507. </View>
  508. );
  509. }
  510. }
  511. export default withTheme(ProxiwashScreen);