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.

PlanexScreen.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. // @flow
  2. import * as React from 'react';
  3. import ThemeManager from "../../managers/ThemeManager";
  4. import WebViewScreen from "../../components/Screens/WebViewScreen";
  5. import {Avatar, Banner, withTheme} from "react-native-paper";
  6. import i18n from "i18n-js";
  7. import {Platform, StatusBar, View} from "react-native";
  8. import AsyncStorageManager from "../../managers/AsyncStorageManager";
  9. import AlertDialog from "../../components/Dialog/AlertDialog";
  10. import {withCollapsible} from "../../utils/withCollapsible";
  11. import {dateToString, getTimeOnlyString} from "../../utils/Planning";
  12. import DateManager from "../../managers/DateManager";
  13. import AnimatedBottomBar from "../../components/Custom/AnimatedBottomBar";
  14. import {CommonActions} from "@react-navigation/native";
  15. import ErrorView from "../../components/Custom/ErrorView";
  16. import AnimatedFocusView from "../../components/Custom/AnimatedFocusView";
  17. type Props = {
  18. navigation: Object,
  19. route: Object,
  20. theme: Object,
  21. collapsibleStack: Object,
  22. }
  23. type State = {
  24. bannerVisible: boolean,
  25. dialogVisible: boolean,
  26. dialogTitle: string,
  27. dialogMessage: string,
  28. currentGroup: Object,
  29. }
  30. const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
  31. // // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing
  32. // // Remove alpha from given Jquery node
  33. // function removeAlpha(node) {
  34. // let bg = node.css("background-color");
  35. // if (bg.match("^rgba")) {
  36. // let a = bg.slice(5).split(',');
  37. // // Fix for tooltips with broken background
  38. // if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {
  39. // a[0] = a[1] = a[2] = '255';
  40. // }
  41. // let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';
  42. // node.css("background-color", newBg);
  43. // }
  44. // }
  45. // // Observe for planning DOM changes
  46. // let observer = new MutationObserver(function(mutations) {
  47. // for (let i = 0; i < mutations.length; i++) {
  48. // if (mutations[i]['addedNodes'].length > 0 &&
  49. // ($(mutations[i]['addedNodes'][0]).hasClass("fc-event") || $(mutations[i]['addedNodes'][0]).hasClass("tooltiptopicevent")))
  50. // removeAlpha($(mutations[i]['addedNodes'][0]))
  51. // }
  52. // });
  53. // // observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});
  54. // observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});
  55. // // Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.
  56. // $(".fc-event-container .fc-event").each(function(index) {
  57. // removeAlpha($(this));
  58. // });
  59. // Watch for changes in the calendar and call the remove alpha function
  60. const OBSERVE_MUTATIONS_INJECTED =
  61. 'function removeAlpha(node) {\n' +
  62. ' let bg = node.css("background-color");\n' +
  63. ' if (bg.match("^rgba")) {\n' +
  64. ' let a = bg.slice(5).split(\',\');\n' +
  65. ' // Fix for tooltips with broken background\n' +
  66. ' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' +
  67. ' a[0] = a[1] = a[2] = \'255\';\n' +
  68. ' }\n' +
  69. ' let newBg =\'rgb(\' + a[0] + \',\' + a[1] + \',\' + a[2] + \')\';\n' +
  70. ' node.css("background-color", newBg);\n' +
  71. ' }\n' +
  72. '}\n' +
  73. '// Observe for planning DOM changes\n' +
  74. 'let observer = new MutationObserver(function(mutations) {\n' +
  75. ' for (let i = 0; i < mutations.length; i++) {\n' +
  76. ' if (mutations[i][\'addedNodes\'].length > 0 &&\n' +
  77. ' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' +
  78. ' removeAlpha($(mutations[i][\'addedNodes\'][0]))\n' +
  79. ' }\n' +
  80. '});\n' +
  81. '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
  82. 'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
  83. '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' +
  84. '$(".fc-event-container .fc-event").each(function(index) {\n' +
  85. ' removeAlpha($(this));\n' +
  86. '});';
  87. const FULL_CALENDAR_SETTINGS = `
  88. var calendar = $('#calendar').fullCalendar('getCalendar');
  89. calendar.option({
  90. eventClick: function (data, event, view) {
  91. var message = {
  92. title: data.title,
  93. color: data.color,
  94. start: data.start._d,
  95. end: data.end._d,
  96. };
  97. window.ReactNativeWebView.postMessage(JSON.stringify(message));
  98. }
  99. });`;
  100. const EXEC_COMMAND = `
  101. function execCommand(event) {
  102. alert(JSON.stringify(event));
  103. var data = JSON.parse(event.data);
  104. if (data.action === "setGroup")
  105. displayAde(data.data);
  106. else
  107. $('#calendar').fullCalendar(data.action, data.data);
  108. };`
  109. const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}.fc-toolbar .fc-center{width:100%}.fc-toolbar .fc-center>*{float:none;width:100%;margin:0}#entite{margin-bottom:5px!important}#entite,#groupe{width:calc(100% - 20px);margin:0 10px}#groupe_visibility{width:100%}#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}";
  110. 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}";
  111. const INJECT_STYLE = `
  112. $('head').append('<meta name="viewport" content="width=device-width, initial-scale=0.9">');
  113. $('head').append('<style>` + CUSTOM_CSS + `</style>');
  114. `;
  115. /**
  116. * Class defining the app's Planex screen.
  117. * This screen uses a webview to render the page
  118. */
  119. class PlanexScreen extends React.Component<Props, State> {
  120. webScreenRef: Object;
  121. barRef: Object;
  122. customInjectedJS: string;
  123. /**
  124. * Defines custom injected JavaScript to improve the page display on mobile
  125. */
  126. constructor(props) {
  127. super(props);
  128. this.webScreenRef = React.createRef();
  129. this.barRef = React.createRef();
  130. let currentGroup = AsyncStorageManager.getInstance().preferences.planexCurrentGroup.current;
  131. if (currentGroup === '')
  132. currentGroup = {name: "SELECT GROUP", id: -1};
  133. else {
  134. currentGroup = JSON.parse(currentGroup);
  135. props.navigation.setOptions({title: currentGroup.name})
  136. }
  137. this.state = {
  138. bannerVisible:
  139. AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' &&
  140. AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex',
  141. dialogVisible: false,
  142. dialogTitle: "",
  143. dialogMessage: "",
  144. currentGroup: currentGroup,
  145. };
  146. this.generateInjectedJS(currentGroup.id);
  147. }
  148. componentDidMount() {
  149. this.props.navigation.addListener('focus', this.onScreenFocus);
  150. }
  151. onScreenFocus = () => {
  152. this.handleNavigationParams();
  153. };
  154. handleNavigationParams = () => {
  155. if (this.props.route.params !== undefined) {
  156. if (this.props.route.params.group !== undefined && this.props.route.params.group !== null) {
  157. // reset params to prevent infinite loop
  158. this.selectNewGroup(this.props.route.params.group);
  159. this.props.navigation.dispatch(CommonActions.setParams({group: null}));
  160. }
  161. }
  162. };
  163. selectNewGroup(group: Object) {
  164. this.sendMessage('setGroup', group.id);
  165. this.setState({currentGroup: group});
  166. AsyncStorageManager.getInstance().savePref(
  167. AsyncStorageManager.getInstance().preferences.planexCurrentGroup.key,
  168. JSON.stringify(group));
  169. this.props.navigation.setOptions({title: group.name})
  170. this.generateInjectedJS(group.id);
  171. }
  172. generateInjectedJS(groupID: number) {
  173. this.customInjectedJS = "$(document).ready(function() {"
  174. + OBSERVE_MUTATIONS_INJECTED
  175. + FULL_CALENDAR_SETTINGS
  176. + "displayAde(" + groupID + ");" // Reset Ade
  177. + (DateManager.isWeekend(new Date()) ? "calendar.next()" : "")
  178. + INJECT_STYLE;
  179. if (ThemeManager.getNightMode())
  180. this.customInjectedJS += "$('head').append('<style>" + CUSTOM_CSS_DARK + "</style>');";
  181. this.customInjectedJS +=
  182. 'removeAlpha();'
  183. + '});'
  184. + EXEC_COMMAND
  185. + 'true;'; // Prevents crash on ios
  186. }
  187. // componentWillUpdate(prevProps: Props) {
  188. // if (prevProps.theme.dark !== this.props.theme.dark)
  189. // this.generateInjectedCSS();
  190. // }
  191. /**
  192. * Callback used when closing the banner.
  193. * This hides the banner and saves to preferences to prevent it from reopening
  194. */
  195. onHideBanner = () => {
  196. this.setState({bannerVisible: false});
  197. AsyncStorageManager.getInstance().savePref(
  198. AsyncStorageManager.getInstance().preferences.planexShowBanner.key,
  199. '0'
  200. );
  201. };
  202. /**
  203. * Callback used when the used click on the navigate to settings button.
  204. * This will hide the banner and open the SettingsScreen
  205. *
  206. */
  207. onGoToSettings = () => {
  208. this.onHideBanner();
  209. this.props.navigation.navigate('settings');
  210. };
  211. sendMessage = (action: string, data: any) => {
  212. let command;
  213. if (action === "setGroup")
  214. command = "displayAde(" + data + ")";
  215. else
  216. command = "$('#calendar').fullCalendar('" + action + "', '" + data + "')";
  217. this.webScreenRef.current.injectJavaScript(command + ';true;');
  218. }
  219. onMessage = (event: Object) => {
  220. let data = JSON.parse(event.nativeEvent.data);
  221. let startDate = dateToString(new Date(data.start), true);
  222. let endDate = dateToString(new Date(data.end), true);
  223. let msg = DateManager.getInstance().getTranslatedDate(startDate) + "\n";
  224. msg += getTimeOnlyString(startDate) + ' - ' + getTimeOnlyString(endDate);
  225. this.showDialog(data.title, msg)
  226. };
  227. showDialog = (title: string, message: string) => {
  228. this.setState({
  229. dialogVisible: true,
  230. dialogTitle: title,
  231. dialogMessage: message,
  232. });
  233. };
  234. hideDialog = () => {
  235. this.setState({
  236. dialogVisible: false,
  237. });
  238. };
  239. onScroll = (event: Object) => {
  240. this.barRef.current.onScroll(event);
  241. };
  242. getWebView() {
  243. const showWebview = this.state.currentGroup.id !== -1;
  244. return (
  245. <View style={{height: '100%'}}>
  246. {!showWebview
  247. ? <ErrorView
  248. {...this.props}
  249. icon={'account-clock'}
  250. message={i18n.t("planexScreen.noGroupSelected")}
  251. showRetryButton={false}
  252. />
  253. : null}
  254. <WebViewScreen
  255. ref={this.webScreenRef}
  256. navigation={this.props.navigation}
  257. url={PLANEX_URL}
  258. customJS={this.customInjectedJS}
  259. onMessage={this.onMessage}
  260. onScroll={this.onScroll}
  261. />
  262. </View>
  263. );
  264. }
  265. render() {
  266. const {containerPaddingTop} = this.props.collapsibleStack;
  267. const padding = Platform.OS === 'android' // Fix for android non translucent bar on expo
  268. ? containerPaddingTop - StatusBar.currentHeight
  269. : containerPaddingTop;
  270. return (
  271. <AnimatedFocusView
  272. {...this.props}
  273. >
  274. <Banner
  275. style={{
  276. marginTop: this.state.bannerVisible ? padding : 0,
  277. }}
  278. visible={this.state.bannerVisible}
  279. actions={[
  280. {
  281. label: i18n.t('planexScreen.enableStartOK'),
  282. onPress: this.onGoToSettings,
  283. },
  284. {
  285. label: i18n.t('planexScreen.enableStartCancel'),
  286. onPress: this.onHideBanner,
  287. },
  288. ]}
  289. icon={() => <Avatar.Icon
  290. icon={'information'}
  291. size={40}
  292. />}
  293. >
  294. {i18n.t('planexScreen.enableStartScreen')}
  295. </Banner>
  296. <AlertDialog
  297. visible={this.state.dialogVisible}
  298. onDismiss={this.hideDialog}
  299. title={this.state.dialogTitle}
  300. message={this.state.dialogMessage}/>
  301. {this.props.theme.dark // Force component theme update
  302. ? this.getWebView()
  303. : <View style={{height: '100%'}}>{this.getWebView()}</View>}
  304. <AnimatedBottomBar
  305. {...this.props}
  306. ref={this.barRef}
  307. onPress={this.sendMessage}
  308. seekAttention={this.state.currentGroup.id === -1}
  309. />
  310. </AnimatedFocusView>
  311. );
  312. }
  313. }
  314. export default withCollapsible(withTheme(PlanexScreen));