diff --git a/src/components/Lists/PlanexGroups/GroupListAccordion.js b/src/components/Lists/PlanexGroups/GroupListAccordion.js index dcf4377..154ea71 100644 --- a/src/components/Lists/PlanexGroups/GroupListAccordion.js +++ b/src/components/Lists/PlanexGroups/GroupListAccordion.js @@ -2,96 +2,115 @@ import * as React from 'react'; import {List, withTheme} from 'react-native-paper'; -import {FlatList, View} from "react-native"; -import {stringMatchQuery} from "../../../utils/Search"; -import GroupListItem from "./GroupListItem"; -import AnimatedAccordion from "../../Animations/AnimatedAccordion"; -import type {group, groupCategory} from "../../../screens/Planex/GroupSelectionScreen"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import {FlatList, View} from 'react-native'; +import {stringMatchQuery} from '../../../utils/Search'; +import GroupListItem from './GroupListItem'; +import AnimatedAccordion from '../../Animations/AnimatedAccordion'; +import type { + PlanexGroupType, + PlanexGroupCategoryType, +} from '../../../screens/Planex/GroupSelectionScreen'; +import type {CustomTheme} from '../../../managers/ThemeManager'; -type Props = { - item: groupCategory, - onGroupPress: (group) => void, - onFavoritePress: (group) => void, - currentSearchString: string, - favoriteNumber: number, - height: number, - theme: CustomTheme, -} +type PropsType = { + item: PlanexGroupCategoryType, + onGroupPress: (PlanexGroupType) => void, + onFavoritePress: (PlanexGroupType) => void, + currentSearchString: string, + favoriteNumber: number, + height: number, + theme: CustomTheme, +}; const LIST_ITEM_HEIGHT = 64; -class GroupListAccordion extends React.Component { +class GroupListAccordion extends React.Component { + shouldComponentUpdate(nextProps: PropsType): boolean { + const {props} = this; + return ( + nextProps.currentSearchString !== props.currentSearchString || + nextProps.favoriteNumber !== props.favoriteNumber || + nextProps.item.content.length !== props.item.content.length + ); + } - shouldComponentUpdate(nextProps: Props) { - return (nextProps.currentSearchString !== this.props.currentSearchString) - || (nextProps.favoriteNumber !== this.props.favoriteNumber) - || (nextProps.item.content.length !== this.props.item.content.length); - } + getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => { + const {props} = this; + const onPress = () => { + props.onGroupPress(item); + }; + const onStarPress = () => { + props.onFavoritePress(item); + }; + return ( + + ); + }; - keyExtractor = (item: group) => item.id.toString(); + getData(): Array { + const {props} = this; + const originalData = props.item.content; + const displayData = []; + originalData.forEach((data: PlanexGroupType) => { + if (stringMatchQuery(data.name, props.currentSearchString)) + displayData.push(data); + }); + return displayData; + } - renderItem = ({item}: { item: group }) => { - const onPress = () => this.props.onGroupPress(item); - const onStarPress = () => this.props.onFavoritePress(item); - return ( - - ); - } + itemLayout = ( + data: ?Array, + index: number, + ): {length: number, offset: number, index: number} => ({ + length: LIST_ITEM_HEIGHT, + offset: LIST_ITEM_HEIGHT * index, + index, + }); - getData() { - const originalData = this.props.item.content; - let displayData = []; - for (let i = 0; i < originalData.length; i++) { - if (stringMatchQuery(originalData[i].name, this.props.currentSearchString)) - displayData.push(originalData[i]); - } - return displayData; - } + keyExtractor = (item: PlanexGroupType): string => item.id.toString(); - itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); - - - render() { - const item = this.props.item; - return ( - - - item.id === 0 - ? - : null} - unmountWhenCollapsed={true}// Only render list if expanded for increased performance - opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0} - > - {/*$FlowFixMe*/} - - - - ); - } + render(): React.Node { + const {props} = this; + const {item} = this.props; + return ( + + + item.id === 0 ? ( + + ) : null + } + unmountWhenCollapsed // Only render list if expanded for increased performance + opened={props.item.id === 0 || props.currentSearchString.length > 0}> + {/* $FlowFixMe */} + + + + ); + } } -export default withTheme(GroupListAccordion) \ No newline at end of file +export default withTheme(GroupListAccordion); diff --git a/src/components/Lists/PlanexGroups/GroupListItem.js b/src/components/Lists/PlanexGroups/GroupListItem.js index 4cb9eec..6ba8b5c 100644 --- a/src/components/Lists/PlanexGroups/GroupListItem.js +++ b/src/components/Lists/PlanexGroups/GroupListItem.js @@ -2,65 +2,67 @@ import * as React from 'react'; import {IconButton, List, withTheme} from 'react-native-paper'; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import type {group} from "../../../screens/Planex/GroupSelectionScreen"; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen'; -type Props = { - theme: CustomTheme, - onPress: () => void, - onStarPress: () => void, - item: group, - height: number, -} +type PropsType = { + theme: CustomTheme, + onPress: () => void, + onStarPress: () => void, + item: PlanexGroupType, + height: number, +}; -type State = { - isFav: boolean, -} +type StateType = { + isFav: boolean, +}; -class GroupListItem extends React.Component { +class GroupListItem extends React.Component { + constructor(props: PropsType) { + super(props); + this.state = { + isFav: props.item.isFav !== undefined && props.item.isFav, + }; + } - constructor(props) { - super(props); - this.state = { - isFav: (props.item.isFav !== undefined && props.item.isFav), - } - } + shouldComponentUpdate(prevProps: PropsType, prevState: StateType): boolean { + const {isFav} = this.state; + return prevState.isFav !== isFav; + } - shouldComponentUpdate(prevProps: Props, prevState: State) { - return (prevState.isFav !== this.state.isFav); - } + onStarPress = () => { + const {props} = this; + this.setState((prevState: StateType): StateType => ({ + isFav: !prevState.isFav, + })); + props.onStarPress(); + }; - onStarPress = () => { - this.setState({isFav: !this.state.isFav}); - this.props.onStarPress(); - } - - render() { - const colors = this.props.theme.colors; - return ( - - } - right={props => - } - style={{ - height: this.props.height, - justifyContent: 'center', - }} - /> - ); - } + render(): React.Node { + const {props, state} = this; + const {colors} = props.theme; + return ( + ( + + )} + right={({size, color}: {size: number, color: string}): React.Node => ( + + )} + style={{ + height: props.height, + justifyContent: 'center', + }} + /> + ); + } } export default withTheme(GroupListItem); diff --git a/src/screens/Planex/GroupSelectionScreen.js b/src/screens/Planex/GroupSelectionScreen.js index 3430c24..d288483 100644 --- a/src/screens/Planex/GroupSelectionScreen.js +++ b/src/screens/Planex/GroupSelectionScreen.js @@ -1,44 +1,45 @@ // @flow import * as React from 'react'; -import {Platform} from "react-native"; -import i18n from "i18n-js"; -import {Searchbar} from "react-native-paper"; -import {stringMatchQuery} from "../../utils/Search"; -import WebSectionList from "../../components/Screens/WebSectionList"; -import GroupListAccordion from "../../components/Lists/PlanexGroups/GroupListAccordion"; -import AsyncStorageManager from "../../managers/AsyncStorageManager"; -import {StackNavigationProp} from "@react-navigation/stack"; +import {Platform} from 'react-native'; +import i18n from 'i18n-js'; +import {Searchbar} from 'react-native-paper'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {stringMatchQuery} from '../../utils/Search'; +import WebSectionList from '../../components/Screens/WebSectionList'; +import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion'; +import AsyncStorageManager from '../../managers/AsyncStorageManager'; const LIST_ITEM_HEIGHT = 70; -export type group = { - name: string, - id: number, - isFav: boolean, +export type PlanexGroupType = { + name: string, + id: number, + isFav: boolean, }; -export type groupCategory = { - name: string, - id: number, - content: Array, +export type PlanexGroupCategoryType = { + name: string, + id: number, + content: Array, }; -type Props = { - navigation: StackNavigationProp, -} - -type State = { - currentSearchString: string, - favoriteGroups: Array, +type PropsType = { + navigation: StackNavigationProp, }; -function sortName(a: group | groupCategory, b: group | groupCategory) { - if (a.name.toLowerCase() < b.name.toLowerCase()) - return -1; - if (a.name.toLowerCase() > b.name.toLowerCase()) - return 1; - return 0; +type StateType = { + currentSearchString: string, + favoriteGroups: Array, +}; + +function sortName( + a: PlanexGroupType | PlanexGroupCategoryType, + b: PlanexGroupType | PlanexGroupCategoryType, +): number { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; } const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1'; @@ -47,232 +48,263 @@ const REPLACE_REGEX = /_/g; /** * Class defining planex group selection screen. */ -class GroupSelectionScreen extends React.Component { - - constructor(props: Props) { - super(props); - this.state = { - currentSearchString: '', - favoriteGroups: AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key), - }; +class GroupSelectionScreen extends React.Component { + /** + * Removes the given group from the given array + * + * @param favorites The array containing favorites groups + * @param group The group to remove from the array + */ + static removeGroupFromFavorites( + favorites: Array, + group: PlanexGroupType, + ) { + for (let i = 0; i < favorites.length; i += 1) { + if (group.id === favorites[i].id) { + favorites.splice(i, 1); + break; + } } + } - /** - * Creates the header content - */ - componentDidMount() { - this.props.navigation.setOptions({ - headerTitle: this.getSearchBar, - headerBackTitleVisible: false, - headerTitleContainerStyle: Platform.OS === 'ios' ? - {marginHorizontal: 0, width: '70%'} : - {marginHorizontal: 0, right: 50, left: 50}, - }); - } + /** + * Adds the given group to the given array + * + * @param favorites The array containing favorites groups + * @param group The group to add to the array + */ + static addGroupToFavorites( + favorites: Array, + group: PlanexGroupType, + ) { + const favGroup = {...group}; + favGroup.isFav = true; + favorites.push(favGroup); + favorites.sort(sortName); + } - /** - * Gets the header search bar - * - * @return {*} - */ - getSearchBar = () => { - return ( - - ); + constructor(props: PropsType) { + super(props); + this.state = { + currentSearchString: '', + favoriteGroups: AsyncStorageManager.getObject( + AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key, + ), }; + } - /** - * Callback used when the search changes - * - * @param str The new search string - */ - onSearchStringChange = (str: string) => { - this.setState({currentSearchString: str}) - }; + /** + * Creates the header content + */ + componentDidMount() { + const [navigation] = this.props; + navigation.setOptions({ + headerTitle: this.getSearchBar, + headerBackTitleVisible: false, + headerTitleContainerStyle: + Platform.OS === 'ios' + ? {marginHorizontal: 0, width: '70%'} + : {marginHorizontal: 0, right: 50, left: 50}, + }); + } - /** - * Callback used when clicking an article in the list. - * It opens the modal to show detailed information about the article - * - * @param item The article pressed - */ - onListItemPress = (item: group) => { - this.props.navigation.navigate("planex", { - screen: "index", - params: {group: item} - }); - }; + /** + * Gets the header search bar + * + * @return {*} + */ + getSearchBar = (): React.Node => { + return ( + + ); + }; - /** - * Callback used when the user clicks on the favorite button - * - * @param item The item to add/remove from favorites - */ - onListFavoritePress = (item: group) => { - this.updateGroupFavorites(item); - }; - - /** - * Checks if the given group is in the favorites list - * - * @param group The group to check - * @returns {boolean} - */ - isGroupInFavorites(group: group) { - let isFav = false; - for (let i = 0; i < this.state.favoriteGroups.length; i++) { - if (group.id === this.state.favoriteGroups[i].id) { - isFav = true; - break; - } - } - return isFav; + /** + * Gets a render item for the given article + * + * @param item The article to render + * @return {*} + */ + getRenderItem = ({item}: {item: PlanexGroupCategoryType}): React.Node => { + const {currentSearchString, favoriteGroups} = this.state; + if (this.shouldDisplayAccordion(item)) { + return ( + + ); } + return null; + }; - /** - * Removes the given group from the given array - * - * @param favorites The array containing favorites groups - * @param group The group to remove from the array - */ - removeGroupFromFavorites(favorites: Array, group: group) { - for (let i = 0; i < favorites.length; i++) { - if (group.id === favorites[i].id) { - favorites.splice(i, 1); - break; - } - } + /** + * Replaces underscore by spaces and sets the favorite state of every group in the given category + * + * @param groups The groups to format + * @return {Array} + */ + getFormattedGroups(groups: Array): Array { + return groups.map((group: PlanexGroupType): PlanexGroupType => { + const newGroup = {...group}; + newGroup.name = group.name.replace(REPLACE_REGEX, ' '); + newGroup.isFav = this.isGroupInFavorites(group); + return newGroup; + }); + } + + /** + * Creates the dataset to be used in the FlatList + * + * @param fetchedData + * @return {*} + * */ + createDataset = (fetchedData: { + [key: string]: PlanexGroupCategoryType, + }): Array<{title: string, data: Array}> => { + return [ + { + title: '', + data: this.generateData(fetchedData), + }, + ]; + }; + + /** + * Callback used when the search changes + * + * @param str The new search string + */ + onSearchStringChange = (str: string) => { + this.setState({currentSearchString: str}); + }; + + /** + * Callback used when clicking an article in the list. + * It opens the modal to show detailed information about the article + * + * @param item The article pressed + */ + onListItemPress = (item: PlanexGroupType) => { + const {navigation} = this.props; + navigation.navigate('planex', { + screen: 'index', + params: {group: item}, + }); + }; + + /** + * Callback used when the user clicks on the favorite button + * + * @param item The item to add/remove from favorites + */ + onListFavoritePress = (item: PlanexGroupType) => { + this.updateGroupFavorites(item); + }; + + /** + * Checks if the given group is in the favorites list + * + * @param group The group to check + * @returns {boolean} + */ + isGroupInFavorites(group: PlanexGroupType): boolean { + let isFav = false; + const {favoriteGroups} = this.state; + favoriteGroups.forEach((favGroup: PlanexGroupType) => { + if (group.id === favGroup.id) isFav = true; + }); + return isFav; + } + + /** + * Adds or removes the given group to the favorites list, depending on whether it is already in it or not. + * Favorites are then saved in user preferences + * + * @param group The group to add/remove to favorites + */ + updateGroupFavorites(group: PlanexGroupType) { + const {favoriteGroups} = this.state; + const newFavorites = [...favoriteGroups]; + if (this.isGroupInFavorites(group)) + GroupSelectionScreen.removeGroupFromFavorites(newFavorites, group); + else GroupSelectionScreen.addGroupToFavorites(newFavorites, group); + this.setState({favoriteGroups: newFavorites}); + AsyncStorageManager.set( + AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key, + newFavorites, + ); + } + + /** + * Checks whether to display the given group category, depending on user search query + * + * @param item The group category + * @returns {boolean} + */ + shouldDisplayAccordion(item: PlanexGroupCategoryType): boolean { + const {currentSearchString} = this.state; + let shouldDisplay = false; + for (let i = 0; i < item.content.length; i += 1) { + if (stringMatchQuery(item.content[i].name, currentSearchString)) { + shouldDisplay = true; + break; + } } + return shouldDisplay; + } - /** - * Adds the given group to the given array - * - * @param favorites The array containing favorites groups - * @param group The group to add to the array - */ - addGroupToFavorites(favorites: Array, group: group) { - group.isFav = true; - favorites.push(group); - favorites.sort(sortName); - } + /** + * Generates the dataset to be used in the FlatList. + * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top. + * + * @param fetchedData The raw data fetched from the server + * @returns {[]} + */ + generateData(fetchedData: { + [key: string]: PlanexGroupCategoryType, + }): Array { + const {favoriteGroups} = this.state; + const data = []; + // eslint-disable-next-line flowtype/no-weak-types + (Object.values(fetchedData): Array).forEach( + (category: PlanexGroupCategoryType) => { + const newCat = {...category}; + newCat.content = this.getFormattedGroups(category.content); + data.push(newCat); + }, + ); + data.sort(sortName); + data.unshift({ + name: i18n.t('screens.planex.favorites'), + id: 0, + content: favoriteGroups, + }); + return data; + } - /** - * Adds or removes the given group to the favorites list, depending on whether it is already in it or not. - * Favorites are then saved in user preferences - * - * @param group The group to add/remove to favorites - */ - updateGroupFavorites(group: group) { - let newFavorites = [...this.state.favoriteGroups] - if (this.isGroupInFavorites(group)) - this.removeGroupFromFavorites(newFavorites, group); - else - this.addGroupToFavorites(newFavorites, group); - this.setState({favoriteGroups: newFavorites}) - AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key, newFavorites); - } - - /** - * Checks whether to display the given group category, depending on user search query - * - * @param item The group category - * @returns {boolean} - */ - shouldDisplayAccordion(item: groupCategory) { - let shouldDisplay = false; - for (let i = 0; i < item.content.length; i++) { - if (stringMatchQuery(item.content[i].name, this.state.currentSearchString)) { - shouldDisplay = true; - break; - } - } - return shouldDisplay; - } - - /** - * Gets a render item for the given article - * - * @param item The article to render - * @return {*} - */ - renderItem = ({item}: { item: groupCategory }) => { - if (this.shouldDisplayAccordion(item)) { - return ( - - ); - } else - return null; - }; - - /** - * Generates the dataset to be used in the FlatList. - * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top. - * - * @param fetchedData The raw data fetched from the server - * @returns {[]} - */ - generateData(fetchedData: { [key: string]: groupCategory }) { - let data = []; - for (let key in fetchedData) { - this.formatGroups(fetchedData[key]); - data.push(fetchedData[key]); - } - data.sort(sortName); - data.unshift({name: i18n.t("screens.planex.favorites"), id: 0, content: this.state.favoriteGroups}); - return data; - } - - /** - * Replaces underscore by spaces and sets the favorite state of every group in the given category - * - * @param item The category containing groups to format - */ - formatGroups(item: groupCategory) { - for (let i = 0; i < item.content.length; i++) { - item.content[i].name = item.content[i].name.replace(REPLACE_REGEX, " ") - item.content[i].isFav = this.isGroupInFavorites(item.content[i]); - } - } - - /** - * Creates the dataset to be used in the FlatList - * - * @param fetchedData - * @return {*} - * */ - createDataset = (fetchedData: { [key: string]: groupCategory }) => { - return [ - { - title: '', - data: this.generateData(fetchedData) - } - ]; - } - - render() { - return ( - - ); - } + render(): React.Node { + const {props, state} = this; + return ( + + ); + } } export default GroupSelectionScreen; diff --git a/src/screens/Planex/PlanexScreen.js b/src/screens/Planex/PlanexScreen.js index 04bf89f..483fe7b 100644 --- a/src/screens/Planex/PlanexScreen.js +++ b/src/screens/Planex/PlanexScreen.js @@ -1,37 +1,36 @@ // @flow import * as React from 'react'; -import type {CustomTheme} from "../../managers/ThemeManager"; -import ThemeManager from "../../managers/ThemeManager"; -import WebViewScreen from "../../components/Screens/WebViewScreen"; -import {withTheme} from "react-native-paper"; -import i18n from "i18n-js"; -import {View} from "react-native"; -import AsyncStorageManager from "../../managers/AsyncStorageManager"; -import AlertDialog from "../../components/Dialogs/AlertDialog"; -import {dateToString, getTimeOnlyString} from "../../utils/Planning"; -import DateManager from "../../managers/DateManager"; -import AnimatedBottomBar from "../../components/Animations/AnimatedBottomBar"; -import {CommonActions} from "@react-navigation/native"; -import ErrorView from "../../components/Screens/ErrorView"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {group} from "./GroupSelectionScreen"; -import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; -import MascotPopup from "../../components/Mascot/MascotPopup"; +import {withTheme} from 'react-native-paper'; +import i18n from 'i18n-js'; +import {View} from 'react-native'; +import {CommonActions} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import type {CustomTheme} from '../../managers/ThemeManager'; +import ThemeManager from '../../managers/ThemeManager'; +import WebViewScreen from '../../components/Screens/WebViewScreen'; +import AsyncStorageManager from '../../managers/AsyncStorageManager'; +import AlertDialog from '../../components/Dialogs/AlertDialog'; +import {dateToString, getTimeOnlyString} from '../../utils/Planning'; +import DateManager from '../../managers/DateManager'; +import AnimatedBottomBar from '../../components/Animations/AnimatedBottomBar'; +import ErrorView from '../../components/Screens/ErrorView'; +import type {PlanexGroupType} from './GroupSelectionScreen'; +import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; +import MascotPopup from '../../components/Mascot/MascotPopup'; -type Props = { - navigation: StackNavigationProp, - route: { params: { group: group } }, - theme: CustomTheme, -} - -type State = { - dialogVisible: boolean, - dialogTitle: string, - dialogMessage: string, - currentGroup: group, -} +type PropsType = { + navigation: StackNavigationProp, + route: {params: {group: PlanexGroupType}}, + theme: CustomTheme, +}; +type StateType = { + dialogVisible: boolean, + dialogTitle: string, + dialogMessage: string, + currentGroup: PlanexGroupType, +}; const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; @@ -66,32 +65,32 @@ const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; // Watch for changes in the calendar and call the remove alpha function to prevent invisible events const OBSERVE_MUTATIONS_INJECTED = - 'function removeAlpha(node) {\n' + - ' let bg = node.css("background-color");\n' + - ' if (bg.match("^rgba")) {\n' + - ' let a = bg.slice(5).split(\',\');\n' + - ' // Fix for tooltips with broken background\n' + - ' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' + - ' a[0] = a[1] = a[2] = \'255\';\n' + - ' }\n' + - ' let newBg =\'rgb(\' + a[0] + \',\' + a[1] + \',\' + a[2] + \')\';\n' + - ' node.css("background-color", newBg);\n' + - ' }\n' + - '}\n' + - '// Observe for planning DOM changes\n' + - 'let observer = new MutationObserver(function(mutations) {\n' + - ' for (let i = 0; i < mutations.length; i++) {\n' + - ' if (mutations[i][\'addedNodes\'].length > 0 &&\n' + - ' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' + - ' removeAlpha($(mutations[i][\'addedNodes\'][0]))\n' + - ' }\n' + - '});\n' + - '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + - 'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + - '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' + - '$(".fc-event-container .fc-event").each(function(index) {\n' + - ' removeAlpha($(this));\n' + - '});'; + 'function removeAlpha(node) {\n' + + ' let bg = node.css("background-color");\n' + + ' if (bg.match("^rgba")) {\n' + + " let a = bg.slice(5).split(',');\n" + + ' // Fix for tooltips with broken background\n' + + ' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' + + " a[0] = a[1] = a[2] = '255';\n" + + ' }\n' + + " let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';\n" + + ' node.css("background-color", newBg);\n' + + ' }\n' + + '}\n' + + '// Observe for planning DOM changes\n' + + 'let observer = new MutationObserver(function(mutations) {\n' + + ' for (let i = 0; i < mutations.length; i++) {\n' + + " if (mutations[i]['addedNodes'].length > 0 &&\n" + + ' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' + + " removeAlpha($(mutations[i]['addedNodes'][0]))\n" + + ' }\n' + + '});\n' + + '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + + 'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + + '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' + + '$(".fc-event-container .fc-event").each(function(index) {\n' + + ' removeAlpha($(this));\n' + + '});'; // Overrides default settings to send a message to the webview when clicking on an event const FULL_CALENDAR_SETTINGS = ` @@ -108,272 +107,294 @@ calendar.option({ } });`; -const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}"; -const CUSTOM_CSS_DARK = "body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}"; +const CUSTOM_CSS = + 'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; +const CUSTOM_CSS_DARK = + 'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; const INJECT_STYLE = ` -$('head').append(''); +$('head').append(''); `; /** * Class defining the app's Planex screen. * This screen uses a webview to render the page */ -class PlanexScreen extends React.Component { +class PlanexScreen extends React.Component { + webScreenRef: {current: null | WebViewScreen}; - webScreenRef: { current: null | WebViewScreen }; - barRef: { current: null | AnimatedBottomBar }; + barRef: {current: null | AnimatedBottomBar}; - customInjectedJS: string; + customInjectedJS: string; - /** - * Defines custom injected JavaScript to improve the page display on mobile - */ - constructor(props) { - super(props); - this.webScreenRef = React.createRef(); - this.barRef = React.createRef(); + /** + * Defines custom injected JavaScript to improve the page display on mobile + */ + constructor(props: PropsType) { + super(props); + this.webScreenRef = React.createRef(); + this.barRef = React.createRef(); - let currentGroup = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.planexCurrentGroup.key); - if (currentGroup === '') - currentGroup = {name: "SELECT GROUP", id: -1, isFav: false}; - else { - currentGroup = JSON.parse(currentGroup); - props.navigation.setOptions({title: currentGroup.name}) - } - this.state = { - dialogVisible: false, - dialogTitle: "", - dialogMessage: "", - currentGroup: currentGroup, - }; - this.generateInjectedJS(currentGroup.id); + let currentGroup = AsyncStorageManager.getString( + AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, + ); + if (currentGroup === '') + currentGroup = {name: 'SELECT GROUP', id: -1, isFav: false}; + else { + currentGroup = JSON.parse(currentGroup); + props.navigation.setOptions({title: currentGroup.name}); } + this.state = { + dialogVisible: false, + dialogTitle: '', + dialogMessage: '', + currentGroup, + }; + this.generateInjectedJS(currentGroup.id); + } - /** - * Register for events and show the banner after 2 seconds - */ - componentDidMount() { - this.props.navigation.addListener('focus', this.onScreenFocus); + /** + * Register for events and show the banner after 2 seconds + */ + componentDidMount() { + const {navigation} = this.props; + navigation.addListener('focus', this.onScreenFocus); + } + + /** + * Only update the screen if the dark theme changed + * + * @param nextProps + * @returns {boolean} + */ + shouldComponentUpdate(nextProps: PropsType): boolean { + const {props, state} = this; + if (nextProps.theme.dark !== props.theme.dark) + this.generateInjectedJS(state.currentGroup.id); + return true; + } + + /** + * Gets the Webview, with an error view on top if no group is selected. + * + * @returns {*} + */ + getWebView(): React.Node { + const {props, state} = this; + const showWebview = state.currentGroup.id !== -1; + + return ( + + {!showWebview ? ( + + ) : null} + + + ); + } + + /** + * Callback used when the user clicks on the navigate to settings button. + * This will hide the banner and open the SettingsScreen + */ + onGoToSettings = () => { + const {navigation} = this.props; + navigation.navigate('settings'); + }; + + onScreenFocus = () => { + this.handleNavigationParams(); + }; + + /** + * Sends a FullCalendar action to the web page inside the webview. + * + * @param action The action to perform, as described in the FullCalendar doc https://fullcalendar.io/docs/v3. + * Or "setGroup" with the group id as data to set the selected group + * @param data Data to pass to the action + */ + sendMessage = (action: string, data: string) => { + let command; + if (action === 'setGroup') command = `displayAde(${data})`; + else command = `$('#calendar').fullCalendar('${action}', '${data}')`; + if (this.webScreenRef.current != null) + this.webScreenRef.current.injectJavaScript(`${command};true;`); // Injected javascript must end with true + }; + + /** + * Shows a dialog when the user clicks on an event. + * + * @param event + */ + onMessage = (event: {nativeEvent: {data: string}}) => { + const data: { + start: string, + end: string, + title: string, + color: string, + } = JSON.parse(event.nativeEvent.data); + const startDate = dateToString(new Date(data.start), true); + const endDate = dateToString(new Date(data.end), true); + const startString = getTimeOnlyString(startDate); + const endString = getTimeOnlyString(endDate); + + let msg = `${DateManager.getInstance().getTranslatedDate(startDate)}\n`; + if (startString != null && endString != null) + msg += `${startString} - ${endString}`; + this.showDialog(data.title, msg); + }; + + /** + * Shows a simple dialog to the user. + * + * @param title The dialog's title + * @param message The message to show + */ + showDialog = (title: string, message: string) => { + this.setState({ + dialogVisible: true, + dialogTitle: title, + dialogMessage: message, + }); + }; + + /** + * Hides the dialog + */ + hideDialog = () => { + this.setState({ + dialogVisible: false, + }); + }; + + /** + * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed + * + * @param event + */ + onScroll = (event: SyntheticEvent) => { + if (this.barRef.current != null) this.barRef.current.onScroll(event); + }; + + /** + * If navigations parameters contain a group, set it as selected + */ + handleNavigationParams = () => { + const {props} = this; + if (props.route.params != null) { + if ( + props.route.params.group !== undefined && + props.route.params.group !== null + ) { + // reset params to prevent infinite loop + this.selectNewGroup(props.route.params.group); + props.navigation.dispatch(CommonActions.setParams({group: null})); + } } + }; - /** - * Callback used when the user clicks on the navigate to settings button. - * This will hide the banner and open the SettingsScreen - */ - onGoToSettings = () => this.props.navigation.navigate('settings'); + /** + * Sends the webpage a message with the new group to select and save it to preferences + * + * @param group The group object selected + */ + selectNewGroup(group: PlanexGroupType) { + const {navigation} = this.props; + this.sendMessage('setGroup', group.id.toString()); + this.setState({currentGroup: group}); + AsyncStorageManager.set( + AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, + group, + ); + navigation.setOptions({title: group.name}); + this.generateInjectedJS(group.id); + } - onScreenFocus = () => { - this.handleNavigationParams(); - }; + /** + * Generates custom JavaScript to be injected into the webpage + * + * @param groupID The current group selected + */ + generateInjectedJS(groupID: number) { + this.customInjectedJS = `$(document).ready(function() {${OBSERVE_MUTATIONS_INJECTED}${FULL_CALENDAR_SETTINGS}displayAde(${groupID});${ + // Reset Ade + DateManager.isWeekend(new Date()) ? 'calendar.next()' : '' + }${INJECT_STYLE}`; - /** - * If navigations parameters contain a group, set it as selected - */ - handleNavigationParams = () => { - if (this.props.route.params != null) { - if (this.props.route.params.group !== undefined && this.props.route.params.group !== null) { - // reset params to prevent infinite loop - this.selectNewGroup(this.props.route.params.group); - this.props.navigation.dispatch(CommonActions.setParams({group: null})); - } - } - }; + if (ThemeManager.getNightMode()) + this.customInjectedJS += `$('head').append('');`; - /** - * Sends the webpage a message with the new group to select and save it to preferences - * - * @param group The group object selected - */ - selectNewGroup(group: group) { - this.sendMessage('setGroup', group.id); - this.setState({currentGroup: group}); - AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, group); - this.props.navigation.setOptions({title: group.name}); - this.generateInjectedJS(group.id); - } + this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios + } - /** - * Generates custom JavaScript to be injected into the webpage - * - * @param groupID The current group selected - */ - generateInjectedJS(groupID: number) { - this.customInjectedJS = "$(document).ready(function() {" - + OBSERVE_MUTATIONS_INJECTED - + FULL_CALENDAR_SETTINGS - + "displayAde(" + groupID + ");" // Reset Ade - + (DateManager.isWeekend(new Date()) ? "calendar.next()" : "") - + INJECT_STYLE; - - if (ThemeManager.getNightMode()) - this.customInjectedJS += "$('head').append('');"; - - this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios - } - - /** - * Only update the screen if the dark theme changed - * - * @param nextProps - * @returns {boolean} - */ - shouldComponentUpdate(nextProps: Props): boolean { - if (nextProps.theme.dark !== this.props.theme.dark) - this.generateInjectedJS(this.state.currentGroup.id); - return true; - } - - - /** - * Sends a FullCalendar action to the web page inside the webview. - * - * @param action The action to perform, as described in the FullCalendar doc https://fullcalendar.io/docs/v3. - * Or "setGroup" with the group id as data to set the selected group - * @param data Data to pass to the action - */ - sendMessage = (action: string, data: any) => { - let command; - if (action === "setGroup") - command = "displayAde(" + data + ")"; - else - command = "$('#calendar').fullCalendar('" + action + "', '" + data + "')"; - if (this.webScreenRef.current != null) - this.webScreenRef.current.injectJavaScript(command + ';true;'); // Injected javascript must end with true - }; - - /** - * Shows a dialog when the user clicks on an event. - * - * @param event - */ - onMessage = (event: { nativeEvent: { data: string } }) => { - const data: { start: string, end: string, title: string, color: string } = JSON.parse(event.nativeEvent.data); - const startDate = dateToString(new Date(data.start), true); - const endDate = dateToString(new Date(data.end), true); - const startString = getTimeOnlyString(startDate); - const endString = getTimeOnlyString(endDate); - - let msg = DateManager.getInstance().getTranslatedDate(startDate) + "\n"; - if (startString != null && endString != null) - msg += startString + ' - ' + endString; - this.showDialog(data.title, msg) - }; - - /** - * Shows a simple dialog to the user. - * - * @param title The dialog's title - * @param message The message to show - */ - showDialog = (title: string, message: string) => { - this.setState({ - dialogVisible: true, - dialogTitle: title, - dialogMessage: message, - }); - }; - - /** - * Hides the dialog - */ - hideDialog = () => { - this.setState({ - dialogVisible: false, - }); - }; - - /** - * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed - * - * @param event - */ - onScroll = (event: SyntheticEvent) => { - if (this.barRef.current != null) - this.barRef.current.onScroll(event); - }; - - /** - * Gets the Webview, with an error view on top if no group is selected. - * - * @returns {*} - */ - getWebView() { - const showWebview = this.state.currentGroup.id !== -1; - - return ( - - {!showWebview - ? - : null} - - - ); - } - - render() { - return ( - - {/*Allow to draw webview bellow banner*/} - - {this.props.theme.dark // Force component theme update by recreating it on theme change - ? this.getWebView() - : {this.getWebView()}} - - {AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key) - .toLowerCase() !== 'planex' - ? : null } - - - - ); - } + render(): React.Node { + const {props, state} = this; + return ( + + {/* Allow to draw webview bellow banner */} + + {props.theme.dark ? ( // Force component theme update by recreating it on theme change + this.getWebView() + ) : ( + {this.getWebView()} + )} + + {AsyncStorageManager.getString( + AsyncStorageManager.PREFERENCES.defaultStartScreen.key, + ).toLowerCase() !== 'planex' ? ( + + ) : null} + + + + ); + } } export default withTheme(PlanexScreen);