diff --git a/assets/mascot/mascot.png b/assets/mascot/mascot.png new file mode 100644 index 0000000..023c9a0 Binary files /dev/null and b/assets/mascot/mascot.png differ diff --git a/assets/mascot/mascot_eyes_cute.png b/assets/mascot/mascot_eyes_cute.png new file mode 100644 index 0000000..41e9716 Binary files /dev/null and b/assets/mascot/mascot_eyes_cute.png differ diff --git a/assets/mascot/mascot_eyes_girly.png b/assets/mascot/mascot_eyes_girly.png new file mode 100644 index 0000000..23b2eea Binary files /dev/null and b/assets/mascot/mascot_eyes_girly.png differ diff --git a/assets/mascot/mascot_eyes_normal.png b/assets/mascot/mascot_eyes_normal.png new file mode 100644 index 0000000..ba4d06c Binary files /dev/null and b/assets/mascot/mascot_eyes_normal.png differ diff --git a/assets/mascot/mascot_eyes_wink.png b/assets/mascot/mascot_eyes_wink.png new file mode 100644 index 0000000..b0ff944 Binary files /dev/null and b/assets/mascot/mascot_eyes_wink.png differ diff --git a/assets/mascot/mascot_glasses.png b/assets/mascot/mascot_glasses.png new file mode 100644 index 0000000..78de4df Binary files /dev/null and b/assets/mascot/mascot_glasses.png differ diff --git a/src/components/Home/ActionsDashboardItem.js b/src/components/Home/ActionsDashboardItem.js index a4a920e..013124f 100644 --- a/src/components/Home/ActionsDashboardItem.js +++ b/src/components/Home/ActionsDashboardItem.js @@ -1,61 +1,33 @@ // @flow import * as React from 'react'; -import {Avatar, Card, List, withTheme} from 'react-native-paper'; -import {StyleSheet, View} from "react-native"; +import {List, withTheme} from 'react-native-paper'; +import {View} from "react-native"; import type {CustomTheme} from "../../managers/ThemeManager"; import i18n from 'i18n-js'; import {StackNavigationProp} from "@react-navigation/stack"; -const ICON_AMICALE = require("../../../assets/amicale.png"); - type Props = { navigation: StackNavigationProp, theme: CustomTheme, - isLoggedIn: boolean, } class ActionsDashBoardItem extends React.Component { shouldComponentUpdate(nextProps: Props): boolean { - return (nextProps.theme.dark !== this.props.theme.dark) - || (nextProps.isLoggedIn !== this.props.isLoggedIn); + return (nextProps.theme.dark !== this.props.theme.dark); } render() { - const isLoggedIn = this.props.isLoggedIn; return ( - - } - right={props => } - onPress={isLoggedIn - ? () => this.props.navigation.navigate("profile") - : () => this.props.navigation.navigate("login", {nextScreen: "profile"})} - style={styles.list} - /> - } right={props => } onPress={() => this.props.navigation.navigate("feedback")} - style={{...styles.list, marginLeft: 10, marginRight: 10}} + style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} /> @@ -63,22 +35,4 @@ class ActionsDashBoardItem extends React.Component { } } -const styles = StyleSheet.create({ - card: { - width: 'auto', - margin: 10, - borderWidth: 1, - }, - avatar: { - backgroundColor: 'transparent', - marginTop: 'auto', - marginBottom: 'auto', - }, - list: { - // height: 50, - paddingTop: 0, - paddingBottom: 0, - } -}); - export default withTheme(ActionsDashBoardItem); diff --git a/src/components/Mascot/Mascot.js b/src/components/Mascot/Mascot.js new file mode 100644 index 0000000..6977779 --- /dev/null +++ b/src/components/Mascot/Mascot.js @@ -0,0 +1,148 @@ +// @flow + +import * as React from 'react'; +import * as Animatable from "react-native-animatable"; +import {Image, View} from "react-native-animatable"; + +type Props = { + size: number, + emotion: number, + animated: boolean, +} + +const MASCOT_IMAGE = require("../../../assets/mascot/mascot.png"); +const MASCOT_EYES_NORMAL = require("../../../assets/mascot/mascot_eyes_normal.png"); +const MASCOT_EYES_GIRLY = require("../../../assets/mascot/mascot_eyes_girly.png"); +const MASCOT_EYES_CUTE = require("../../../assets/mascot/mascot_eyes_cute.png"); +const MASCOT_EYES_WINK = require("../../../assets/mascot/mascot_eyes_wink.png"); +const MASCOT_GLASSES = require("../../../assets/mascot/mascot_glasses.png"); + +export const EYE_STYLE = { + NORMAL: 0, + GIRLY: 2, + CUTE: 3, + WINK: 4, +} + +export const MASCOT_STYLE = { + NORMAL: 0, + HAPPY: 1, + GIRLY: 2, + WINK: 3, + CUTE: 4, + INTELLO: 5, +}; + + +class Mascot extends React.Component { + + static defaultProps = { + animated: false + } + + eyeList: { [key: number]: number | string } + + constructor(props: Props) { + super(props); + this.eyeList = {}; + this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; + this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; + this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; + this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK; + } + + getGlasses() { + return + } + + getEye(style: number, isRight: boolean) { + const eye = this.eyeList[style]; + return + } + + getEyes(emotion: number) { + let final = []; + final.push(); + if (emotion === MASCOT_STYLE.CUTE) { + final.push(this.getEye(EYE_STYLE.CUTE, true)); + final.push(this.getEye(EYE_STYLE.CUTE, false)); + } else if (emotion === MASCOT_STYLE.GIRLY) { + final.push(this.getEye(EYE_STYLE.GIRLY, true)); + final.push(this.getEye(EYE_STYLE.GIRLY, false)); + } else if (emotion === MASCOT_STYLE.HAPPY) { + final.push(this.getEye(EYE_STYLE.WINK, true)); + final.push(this.getEye(EYE_STYLE.WINK, false)); + } else if (emotion === MASCOT_STYLE.WINK) { + final.push(this.getEye(EYE_STYLE.WINK, true)); + final.push(this.getEye(EYE_STYLE.NORMAL, false)); + } else { + final.push(this.getEye(EYE_STYLE.NORMAL, true)); + final.push(this.getEye(EYE_STYLE.NORMAL, false)); + } + + if (emotion === MASCOT_STYLE.INTELLO) { + final.push(this.getGlasses()) + } + final.push(); + return final; + } + + render() { + const size = this.props.size; + return ( + + + + {this.getEyes(this.props.emotion)} + + + ); + } +} + +export default Mascot; diff --git a/src/components/Mascot/MascotPopup.js b/src/components/Mascot/MascotPopup.js new file mode 100644 index 0000000..d267e8e --- /dev/null +++ b/src/components/Mascot/MascotPopup.js @@ -0,0 +1,225 @@ +// @flow + +import * as React from 'react'; +import {Avatar, Button, Card, Paragraph, Portal, withTheme} from 'react-native-paper'; +import Mascot from "./Mascot"; +import * as Animatable from "react-native-animatable"; +import {Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native"; +import type {CustomTheme} from "../../managers/ThemeManager"; + +type Props = { + visible: boolean, + theme: CustomTheme, + icon: string, + title: string, + message: string, + buttons: { + action: { + message: string, + icon: string | null, + color: string | null, + onPress: () => void, + }, + cancel: { + message: string, + icon: string | null, + color: string | null, + onPress: () => void, + } + }, + emotion: number, +} + +type State = { + shouldShowDialog: boolean; +} + + +class MascotPopup extends React.Component { + + mascotSize: number; + windowWidth: number; + windowHeight: number; + + state = { + shouldShowDialog: this.props.visible, + }; + + + constructor(props: Props) { + super(props); + + this.windowWidth = Dimensions.get('window').width; + this.windowHeight = Dimensions.get('window').height; + + this.mascotSize = Dimensions.get('window').height / 6; + } + + onAnimationEnd = () => { + this.setState({ + shouldShowDialog: this.props.visible, + }) + } + + shouldComponentUpdate(nextProps: Props): boolean { + if (nextProps.visible) + this.state.shouldShowDialog = true; + else if (nextProps.visible !== this.props.visible) + setTimeout(this.onAnimationEnd, 300); + return true; + } + + getSpeechBubble() { + return ( + + + + + + : null} + /> + + + + + {this.props.message} + + + + + + {this.getButtons()} + + + + ); + } + + getMascot() { + return ( + + + + ); + } + + getButtons() { + const action = this.props.buttons.action; + const cancel = this.props.buttons.cancel; + return ( + + {cancel != null + ? + : null} + {action != null + ? + : null} + + ); + } + + getBackground() { + return ( + + + + + ); + } + + render() { + if (this.state.shouldShowDialog) { + return ( + + {this.getBackground()} + + {this.getMascot()} + {this.getSpeechBubble()} + + + ); + } else + return null; + + } +} + +export default withTheme(MascotPopup); diff --git a/src/managers/ThemeManager.js b/src/managers/ThemeManager.js index 1c7e398..c68c57a 100644 --- a/src/managers/ThemeManager.js +++ b/src/managers/ThemeManager.js @@ -55,6 +55,9 @@ export type CustomTheme = { tetrisZ: string, tetrisJ: string, tetrisL: string, + + // Mascot Popup + mascotMessageArrow: string, }, } @@ -83,7 +86,7 @@ export default class ThemeManager { primary: '#be1522', accent: '#be1522', tabIcon: "#929292", - card: "rgb(255, 255, 255)", + card: "#fff", dividerBackground: '#e2e2e2', ripple: "rgba(0,0,0,0.2)", textDisabled: '#c1c1c1', @@ -126,6 +129,9 @@ export default class ThemeManager { tetrisZ: '#ff0009', tetrisJ: '#2a67e3', tetrisL: '#da742d', + + // Mascot Popup + mascotMessageArrow: "#dedede", }, }; } @@ -144,7 +150,7 @@ export default class ThemeManager { accent: '#be1522', tabBackground: "#181818", tabIcon: "#6d6d6d", - card: "rgb(18, 18, 18)", + card: "rgb(18,18,18)", dividerBackground: '#222222', ripple: "rgba(255,255,255,0.2)", textDisabled: '#5b5b5b', @@ -186,6 +192,9 @@ export default class ThemeManager { tetrisZ: '#b50008', tetrisJ: '#0f37b9', tetrisL: '#b96226', + + // Mascot Popup + mascotMessageArrow: "#323232", }, }; } diff --git a/src/screens/Home/HomeScreen.js b/src/screens/Home/HomeScreen.js index 54b660e..3254a8b 100644 --- a/src/screens/Home/HomeScreen.js +++ b/src/screens/Home/HomeScreen.js @@ -5,7 +5,7 @@ import {FlatList} from 'react-native'; import i18n from "i18n-js"; import DashboardItem from "../../components/Home/EventDashboardItem"; import WebSectionList from "../../components/Screens/WebSectionList"; -import {Avatar, Banner, withTheme} from 'react-native-paper'; +import {withTheme} from 'react-native-paper'; import FeedItem from "../../components/Home/FeedItem"; import SquareDashboardItem from "../../components/Home/SmallDashboardItem"; import PreviewEventDashboardItem from "../../components/Home/PreviewEventDashboardItem"; @@ -19,10 +19,10 @@ import type {CustomTheme} from "../../managers/ThemeManager"; import {View} from "react-native-animatable"; import ConnectionManager from "../../managers/ConnectionManager"; import LogoutDialog from "../../components/Amicale/LogoutDialog"; -import {withCollapsible} from "../../utils/withCollapsible"; -import {Collapsible} from "react-navigation-collapsible"; import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AvailableWebsites from "../../constants/AvailableWebsites"; +import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; +import MascotPopup from "../../components/Mascot/MascotPopup"; // import DATA from "../dashboard_data.json"; @@ -97,12 +97,11 @@ type Props = { navigation: StackNavigationProp, route: { params: any, ... }, theme: CustomTheme, - collapsibleStack: Collapsible, } type State = { dialogVisible: boolean, - bannerVisible: boolean, + mascotDialogVisible: boolean, } /** @@ -115,16 +114,19 @@ class HomeScreen extends React.Component { fabRef: { current: null | AnimatedFAB }; currentNewFeed: Array; - state = { - dialogVisible: false, - bannerVisible: false, - } - constructor(props) { super(props); this.fabRef = React.createRef(); this.currentNewFeed = []; - this.isLoggedIn = null; + this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); + this.props.navigation.setOptions({ + headerRight: this.getHeaderButton, + }); + this.state = { + dialogVisible: false, + mascotDialogVisible: AsyncStorageManager.getInstance().preferences.homeShowBanner.current === "1" + && !this.isLoggedIn, + } } /** @@ -142,13 +144,6 @@ class HomeScreen extends React.Component { this.props.navigation.addListener('focus', this.onScreenFocus); // Handle link open when home is focused this.props.navigation.addListener('state', this.handleNavigationParams); - setTimeout(this.onBannerTimeout, 2000); - } - - onBannerTimeout = () => { - this.setState({ - bannerVisible: AsyncStorageManager.getInstance().preferences.homeShowBanner.current === "1" - }) } /** @@ -161,9 +156,6 @@ class HomeScreen extends React.Component { headerRight: this.getHeaderButton, }); } - if (this.isLoggedIn) { - this.setState({bannerVisible: false}) - } // handle link open when home is not focused or created this.handleNavigationParams(); }; @@ -203,6 +195,14 @@ class HomeScreen extends React.Component { ; }; + hideMascotDialog = () => { + AsyncStorageManager.getInstance().savePref( + AsyncStorageManager.getInstance().preferences.homeShowBanner.key, + '0' + ); + this.setState({mascotDialogVisible: false}) + }; + showDisconnectDialog = () => this.setState({dialogVisible: true}); hideDisconnectDialog = () => this.setState({dialogVisible: false}); @@ -569,29 +569,16 @@ class HomeScreen extends React.Component { this.fabRef.current.onScroll(event); }; - /** - * Callback used when closing the banner. - * This hides the banner and saves to preferences to prevent it from reopening. - */ - onHideBanner = () => { - this.setState({bannerVisible: false}); - AsyncStorageManager.getInstance().savePref( - AsyncStorageManager.getInstance().preferences.homeShowBanner.key, - '0' - ); - }; - /** * Callback when pressing the login button on the banner. * This hides the banner and takes the user to the login page. */ - onLoginBanner = () => { - this.onHideBanner(); + onLogin = () => { + this.hideMascotDialog(); this.props.navigation.navigate("login", {nextScreen: "profile"}); } render() { - const {containerPaddingTop} = this.props.collapsibleStack; return ( { showError={false} /> + { visible={this.state.dialogVisible} onDismiss={this.hideDisconnectDialog} /> - } - > - {i18n.t('homeScreen.loginBanner.message')} - ); } } -export default withCollapsible(withTheme(HomeScreen)); +export default withTheme(HomeScreen); diff --git a/src/screens/Planex/PlanexScreen.js b/src/screens/Planex/PlanexScreen.js index 07e75a7..c2c5d6e 100644 --- a/src/screens/Planex/PlanexScreen.js +++ b/src/screens/Planex/PlanexScreen.js @@ -4,30 +4,29 @@ import * as React from 'react'; import type {CustomTheme} from "../../managers/ThemeManager"; import ThemeManager from "../../managers/ThemeManager"; import WebViewScreen from "../../components/Screens/WebViewScreen"; -import {Avatar, Banner, withTheme} from "react-native-paper"; +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 {withCollapsible} from "../../utils/withCollapsible"; 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 {Collapsible} from "react-navigation-collapsible"; import type {group} 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, - collapsibleStack: Collapsible, } type State = { - bannerVisible: boolean, + mascotDialogVisible: boolean, dialogVisible: boolean, dialogTitle: string, dialogMessage: string, @@ -144,7 +143,9 @@ class PlanexScreen extends React.Component { props.navigation.setOptions({title: currentGroup.name}) } this.state = { - bannerVisible: false, + mascotDialogVisible: + AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' && + AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex', dialogVisible: false, dialogTitle: "", dialogMessage: "", @@ -158,23 +159,14 @@ class PlanexScreen extends React.Component { */ componentDidMount() { this.props.navigation.addListener('focus', this.onScreenFocus); - setTimeout(this.onBannerTimeout, 2000); - } - - onBannerTimeout = () => { - this.setState({ - bannerVisible: - AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' && - AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex' - }) } /** * Callback used when closing the banner. * This hides the banner and saves to preferences to prevent it from reopening */ - onHideBanner = () => { - this.setState({bannerVisible: false}); + onMascotDialogCancel = () => { + this.setState({mascotDialogVisible: false}); AsyncStorageManager.getInstance().savePref( AsyncStorageManager.getInstance().preferences.planexShowBanner.key, '0' @@ -187,7 +179,7 @@ class PlanexScreen extends React.Component { * This will hide the banner and open the SettingsScreen */ onGoToSettings = () => { - this.onHideBanner(); + this.onMascotDialogCancel(); this.props.navigation.navigate('settings'); }; @@ -356,7 +348,6 @@ class PlanexScreen extends React.Component { } render() { - const {containerPaddingTop} = this.props.collapsibleStack; return ( { ? this.getWebView() : {this.getWebView()}} - } - > - {i18n.t('planexScreen.enableStartScreen')} - + cancel: { + message: i18n.t("planexScreen.enableStartCancel"), + icon: "close", + color: this.props.theme.colors.warning, + onPress: this.onMascotDialogCancel, + } + }} + emotion={MASCOT_STYLE.INTELLO} + /> { } } -export default withCollapsible(withTheme(PlanexScreen)); +export default withTheme(PlanexScreen); diff --git a/src/screens/Proxiwash/ProxiwashScreen.js b/src/screens/Proxiwash/ProxiwashScreen.js index 3d6df27..522cde4 100644 --- a/src/screens/Proxiwash/ProxiwashScreen.js +++ b/src/screens/Proxiwash/ProxiwashScreen.js @@ -6,19 +6,19 @@ import i18n from "i18n-js"; import WebSectionList from "../../components/Screens/WebSectionList"; import * as Notifications from "../../utils/Notifications"; import AsyncStorageManager from "../../managers/AsyncStorageManager"; -import {Avatar, Banner, Button, Card, Text, withTheme} from 'react-native-paper'; +import {Avatar, Button, Card, Text, withTheme} from 'react-native-paper'; import ProxiwashListItem from "../../components/Lists/Proxiwash/ProxiwashListItem"; import ProxiwashConstants from "../../constants/ProxiwashConstants"; import CustomModal from "../../components/Overrides/CustomModal"; import AprilFoolsManager from "../../managers/AprilFoolsManager"; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import ProxiwashSectionHeader from "../../components/Lists/Proxiwash/ProxiwashSectionHeader"; -import {withCollapsible} from "../../utils/withCollapsible"; import type {CustomTheme} from "../../managers/ThemeManager"; -import {Collapsible} from "react-navigation-collapsible"; import {StackNavigationProp} from "@react-navigation/stack"; import {getCleanedMachineWatched, getMachineEndDate, isMachineWatched} from "../../utils/Proxiwash"; import {Modalize} from "react-native-modalize"; +import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; +import MascotPopup from "../../components/Mascot/MascotPopup"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json"; @@ -40,14 +40,13 @@ export type Machine = { type Props = { navigation: StackNavigationProp, theme: CustomTheme, - collapsibleStack: Collapsible, } type State = { refreshing: boolean, modalCurrentDisplayItem: React.Node, machinesWatched: Array, - bannerVisible: boolean, + mascotDialogVisible: boolean, }; @@ -68,7 +67,7 @@ class ProxiwashScreen extends React.Component { refreshing: false, modalCurrentDisplayItem: null, machinesWatched: JSON.parse(AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current), - bannerVisible: false, + mascotDialogVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === "1", }; /** @@ -89,8 +88,8 @@ class ProxiwashScreen extends React.Component { * Callback used when closing the banner. * This hides the banner and saves to preferences to prevent it from reopening */ - onHideBanner = () => { - this.setState({bannerVisible: false}); + onHideMascotDialog = () => { + this.setState({mascotDialogVisible: false}); AsyncStorageManager.getInstance().savePref( AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.key, '0' @@ -107,11 +106,6 @@ class ProxiwashScreen extends React.Component { , }); - setTimeout(this.onBannerTimeout, 2000); - } - - onBannerTimeout = () => { - this.setState({bannerVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === "1"}) } /** @@ -401,7 +395,6 @@ class ProxiwashScreen extends React.Component { render() { const nav = this.props.navigation; - const {containerPaddingTop} = this.props.collapsibleStack; return ( { refreshOnFocus={true} updateData={this.state.machinesWatched.length}/> - } - > - {i18n.t('proxiwashScreen.enableNotificationsTip')} - + emotion={MASCOT_STYLE.NORMAL} + /> {this.state.modalCurrentDisplayItem} @@ -448,4 +437,4 @@ class ProxiwashScreen extends React.Component { } } -export default withCollapsible(withTheme(ProxiwashScreen)); +export default withTheme(ProxiwashScreen); diff --git a/translations/en.json b/translations/en.json index 57c5b1f..3e77ae3 100644 --- a/translations/en.json +++ b/translations/en.json @@ -116,7 +116,8 @@ "loginBanner": { "login": "Login", "later": "Later", - "message": "Login to your Amicale account to get access to more services!" + "title": "Welcome, you!", + "message": "Login to your Amicale account to get access to more services!\n\nYou will still be able to login later." } }, "aboutScreen": { @@ -179,9 +180,10 @@ "dryerTips": "The advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.", "procedure": "Procedure", "tips": "Tips", - "enableNotificationsTip": "Click on a running machine to enable notifications", + "enableNotificationsTip": "Click on a running machine to enable notifications!\n\nYou will never forget your laundry again.", "numAvailable": "available", "numAvailablePlural": "available", + "bannerTitle": "Notifications!", "bannerButton": "Got it!", "modal": { "enableNotifications": "Notify me", @@ -215,7 +217,8 @@ } }, "planexScreen": { - "enableStartScreen": "Come here often? Set it as default screen!", + "enableStartScreenTitle": "Come here often?", + "enableStartScreenMessage": "Set it as default screen!\n\nCampus will start on Planex so you never miss a class. Click on the button bellow to navigate to the settings page.", "enableStartOK": "Yes please!", "enableStartCancel": "Later", "noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",