Updated intro slides to make them shorter and include the mascot

This commit is contained in:
Arnaud Vergnet 2020-07-13 20:09:28 +02:00
parent 90d1437248
commit 434d8b6565
4 changed files with 340 additions and 132 deletions

View file

@ -379,18 +379,10 @@
"title": "Stay up to date",
"text": "CAMPUS allows you to be aware of any event occurring on the campus, from pancake sales to Enfoiros concerts!"
},
"slideProxiwash": {
"title": "Never forget your laundry",
"text": "CAMPUS will inform you on the availability of washing machines and will remind you just before yours finishes!"
},
"slidePlanex": {
"title": "Planex",
"text": "Lookup your next course on CAMPUS with a mobile friendly timetable"
},
"slideRU": {
"title": "RU Menu",
"text": "For the hungry, check this week's menu!"
},
"slideServices": {
"title": "More services!",
"text": "You can do much more with CAMPUS, explore the app to find out"

View file

@ -372,32 +372,24 @@
},
"intro": {
"slideMain": {
"title": "Bienvenue sur CAMPUS",
"text": "La nouvelle appli à consulter pendant la pause café pour être au courant de la vie du campus !"
},
"slideEvents": {
"title": "Restez informés",
"text": "CAMPUS vous permet d'être au courant de tous les événements qui ont lieu sur le campus, de la vente de crêpes jusqu'aux concerts enfoiros !"
},
"slideProxiwash": {
"title": "N'oubliez plus votre linge !",
"text": "CAMPUS vous informe de la disponibilité des machines et vous permet d'être notifié lorsque la vôtre se termine bientôt !"
"title": "Bienvenue sur CAMPUS !",
"text": "L'appli du campus de l'INSA Toulouse ! Laisse toi guider pour comprendre tout ce que tu peux faire."
},
"slidePlanex": {
"title": "Planex",
"text": "Vérifiez votre prochain cours sur CAMPUS avec un emploi du temps adapté mobile"
"title": "Planex tout beau",
"text": "Regarde ton emploi du temps et celui de tes amis avec un Planex adapté mobile !"
},
"slideRU": {
"title": "Menu du RU",
"text": "Pour ceux qui ont faim, vérifiez le menu du RU de la semaine !"
"slideEvents": {
"title": "Plein d'infos",
"text": "Sois au courant de tout ce qui se passe sur le campus, de la vente de crêpes jusqu'aux concerts Enfoiros !"
},
"slideServices": {
"title": "Encore plus de services !",
"text": "CAMPUS vous permet de faire bien plus, explorez l'appli pour savoir quoi"
"title": "Et plus encore !",
"text": "Tu peux faire bien plus avec CAMPUS, mais je n'ai pas le temps de tout dire ici. Balade toi sur l'appli pour tout découvrir !"
},
"slideDone": {
"title": "Fait par un étudiant",
"text": "Cette appli à été réalisée par un seul étudiant (avec un peu d'aide par-ci par-là), donc tous les retours sont les bienvenus !"
"title": "Réalisé par un étudiant",
"text": "Cette appli à été réalisée par un seul étudiant (avec un peu d'aide par-ci par-là), donc tes retours sont les bienvenus !"
},
"updateSlide0": {
"title": "Nouveau dans cette mise à jour !",

View file

@ -2,26 +2,46 @@
import * as React from 'react';
import * as Animatable from "react-native-animatable";
import {Image, View} from "react-native-animatable";
import {Image, TouchableWithoutFeedback, View} from "react-native";
type Props = {
size: number,
emotion: number,
animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null,
loopAnimation: Animatable.AnimatableProperties | null,
onPress?: (viewRef: AnimatableViewRef) => null,
onLongPress?: (viewRef: AnimatableViewRef) => null,
}
type State = {
currentEmotion: number,
}
export type AnimatableViewRef = {current: null | Animatable.View};
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_EYES_HEART = require("../../../assets/mascot/mascot_eyes_heart.png");
const MASCOT_EYES_ANGRY = require("../../../assets/mascot/mascot_eyes_angry.png");
const MASCOT_GLASSES = require("../../../assets/mascot/mascot_glasses.png");
const MASCOT_SUNGLASSES = require("../../../assets/mascot/mascot_sunglasses.png");
export const EYE_STYLE = {
NORMAL: 0,
GIRLY: 2,
CUTE: 3,
WINK: 4,
HEART: 5,
ANGRY: 6,
}
const GLASSES_STYLE = {
NORMAL: 0,
COOl: 1
}
export const MASCOT_STYLE = {
@ -31,30 +51,96 @@ export const MASCOT_STYLE = {
WINK: 3,
CUTE: 4,
INTELLO: 5,
LOVE: 6,
COOL: 7,
ANGRY: 8,
};
class Mascot extends React.Component<Props> {
class Mascot extends React.Component<Props, State> {
static defaultProps = {
animated: false
state = {
currentEmotion: this.props.emotion
}
eyeList: { [key: number]: number | string }
static defaultProps = {
animated: false,
entryAnimation: {
useNativeDriver: true,
animation: "rubberBand",
duration: 2000,
},
loopAnimation: {
useNativeDriver: true,
animation: "swing",
duration: 2000,
iterationDelay: 250,
iterationCount: "infinite",
},
clickAnimation: {
useNativeDriver: true,
animation: "rubberBand",
duration: 2000,
},
}
viewRef: AnimatableViewRef;
eyeList: { [key: number]: number | string };
glassesList: { [key: number]: number | string };
onPress: (viewRef: AnimatableViewRef) => null;
onLongPress: (viewRef: AnimatableViewRef) => null;
constructor(props: Props) {
super(props);
this.viewRef = React.createRef();
this.eyeList = {};
this.glassesList = {};
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;
this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART;
this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY;
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES;
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
if (this.props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
viewRef.current.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.props.emotion});
});
}
return null;
}
} else
this.onPress = this.props.onPress;
if (this.props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
viewRef.current.tada(1000).then(() => {
this.setState({currentEmotion: this.props.emotion});
});
}
return null;
}
} else
this.onLongPress = this.props.onLongPress;
}
getGlasses() {
getGlasses(style: number) {
const glasses = this.glassesList[style];
return <Image
key={"glasses"}
source={MASCOT_GLASSES}
source={glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]}
style={{
position: "absolute",
top: "15%",
@ -65,7 +151,7 @@ class Mascot extends React.Component<Props> {
/>
}
getEye(style: number, isRight: boolean) {
getEye(style: number, isRight: boolean, rotation: string="0deg") {
const eye = this.eyeList[style];
return <Image
key={isRight ? "right" : "left"}
@ -76,6 +162,7 @@ class Mascot extends React.Component<Props> {
left: isRight ? "-11%" : "11%",
width: this.props.size,
height: this.props.size,
transform: [{rotateY: rotation}]
}}
/>
}
@ -85,10 +172,10 @@ class Mascot extends React.Component<Props> {
final.push(<View
key={"container"}
style={{
position: "absolute",
width: this.props.size,
height: this.props.size,
}}/>);
position: "absolute",
width: this.props.size,
height: this.props.size,
}}/>);
if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true));
final.push(this.getEye(EYE_STYLE.CUTE, false));
@ -101,13 +188,21 @@ class Mascot extends React.Component<Props> {
} else if (emotion === MASCOT_STYLE.WINK) {
final.push(this.getEye(EYE_STYLE.WINK, true));
final.push(this.getEye(EYE_STYLE.NORMAL, false));
} else if (emotion === MASCOT_STYLE.LOVE) {
final.push(this.getEye(EYE_STYLE.HEART, true));
final.push(this.getEye(EYE_STYLE.HEART, false));
} else if (emotion === MASCOT_STYLE.ANGRY) {
final.push(this.getEye(EYE_STYLE.ANGRY, true));
final.push(this.getEye(EYE_STYLE.ANGRY, false, "180deg"));
} else if (emotion === MASCOT_STYLE.COOL) {
final.push(this.getGlasses(GLASSES_STYLE.COOl));
} 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())
if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses
final.push(this.getGlasses(GLASSES_STYLE.NORMAL));
}
final.push(<View key={"container2"}/>);
return final;
@ -115,31 +210,37 @@ class Mascot extends React.Component<Props> {
render() {
const size = this.props.size;
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
return (
<Animatable.View
style={{
width: size,
height: size,
}}
useNativeDriver={true}
animation={this.props.animated ? "rubberBand" : null}
duration={2000}
{...entryAnimation}
>
<View
useNativeDriver={true}
animation={this.props.animated ? "swing" : null}
duration={2000}
iterationCount={"infinite"}
<TouchableWithoutFeedback
onPress={() => this.onPress(this.viewRef)}
onLongPress={() => this.onLongPress(this.viewRef)}
>
<Image
source={MASCOT_IMAGE}
style={{
width: size,
height: size,
}}
/>
{this.getEyes(this.props.emotion)}
</View>
<Animatable.View
ref={this.viewRef}
>
<Animatable.View
{...loopAnimation}
>
<Image
source={MASCOT_IMAGE}
style={{
width: size,
height: size,
}}
/>
{this.getEyes(this.state.currentEmotion)}
</Animatable.View>
</Animatable.View>
</TouchableWithoutFeedback>
</Animatable.View>
);
}

View file

@ -1,14 +1,17 @@
// @flow
import * as React from 'react';
import {Image, Platform, StatusBar, StyleSheet, View} from "react-native";
import {Platform, StatusBar, StyleSheet, View} from "react-native";
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import {Text} from "react-native-paper";
import i18n from 'i18n-js';
import AppIntroSlider from "react-native-app-intro-slider";
import Update from "../../constants/Update";
import ThemeManager from "../../managers/ThemeManager";
import LinearGradient from 'react-native-linear-gradient';
import Mascot, {MASCOT_STYLE} from "../Mascot/Mascot";
import * as Animatable from "react-native-animatable";
import {Card} from "react-native-paper";
type Props = {
onDone: Function,
@ -16,17 +19,34 @@ type Props = {
isAprilFools: boolean,
};
type State = {
currentSlide: number,
}
type Slide = {
key: string,
title: string,
text: string,
view: () => React.Node,
mascotStyle: number,
colors: [string, string]
};
/**
* Class used to create intro slides
*/
export default class CustomIntroSlider extends React.Component<Props> {
export default class CustomIntroSlider extends React.Component<Props, State> {
sliderRef: {current: null | AppIntroSlider};
state = {
currentSlide: 0,
}
introSlides: Array<Object>;
updateSlides: Array<Object>;
aprilFoolsSlides: Array<Object>;
currentSlides: Array<Object>;
sliderRef: { current: null | AppIntroSlider };
introSlides: Array<Slide>;
updateSlides: Array<Slide>;
aprilFoolsSlides: Array<Slide>;
currentSlides: Array<Slide>;
/**
* Generates intro slides
@ -36,53 +56,44 @@ export default class CustomIntroSlider extends React.Component<Props> {
this.sliderRef = React.createRef();
this.introSlides = [
{
key: 'main',
key: '0', // Mascot
title: i18n.t('intro.slideMain.title'),
text: i18n.t('intro.slideMain.text'),
image: require('../../../assets/splash.png'),
colors: ['#be1522', '#740d15'],
view: this.getWelcomeView,
mascotStyle: MASCOT_STYLE.NORMAL,
colors: ['#be1522', '#57080e'],
},
{
key: 'Planex',
key: '1',
title: i18n.t('intro.slidePlanex.title'),
text: i18n.t('intro.slidePlanex.text'),
icon: 'timetable',
colors: ['#e77020', '#803e12'],
view: () => this.getIconView("calendar-clock"),
mascotStyle: MASCOT_STYLE.INTELLO,
colors: ['#be1522', '#57080e'],
},
{
key: 'RU',
title: i18n.t('intro.slideRU.title'),
text: i18n.t('intro.slideRU.text'),
icon: 'silverware-fork-knife',
colors: ['#dcac18', '#8b6a15'],
},
{
key: 'events',
key: '2',
title: i18n.t('intro.slideEvents.title'),
text: i18n.t('intro.slideEvents.text'),
icon: 'calendar-range',
colors: ['#41a006', '#095c03'],
view: () => this.getIconView("calendar-star",),
mascotStyle: MASCOT_STYLE.HAPPY,
colors: ['#be1522', '#57080e'],
},
{
key: 'proxiwash',
title: i18n.t('intro.slideProxiwash.title'),
text: i18n.t('intro.slideProxiwash.text'),
icon: 'washing-machine',
colors: ['#1fa5ee', '#06537d'],
},
{
key: 'services',
key: '3',
title: i18n.t('intro.slideServices.title'),
text: i18n.t('intro.slideServices.text'),
icon: 'view-dashboard-variant',
colors: ['#6737c1', '#281a5a'],
view: () => this.getIconView("view-dashboard-variant",),
mascotStyle: MASCOT_STYLE.CUTE,
colors: ['#be1522', '#57080e'],
},
{
key: 'done',
key: '4',
title: i18n.t('intro.slideDone.title'),
text: i18n.t('intro.slideDone.text'),
icon: 'account-heart',
colors: ['#b837c1', '#501a5a'],
view: () => this.getIconView("account-heart",),
mascotStyle: MASCOT_STYLE.COOL,
colors: ['#9c165b', '#3e042b'],
},
];
this.updateSlides = [];
@ -103,50 +114,127 @@ export default class CustomIntroSlider extends React.Component<Props> {
key: '1',
title: i18n.t('intro.aprilFoolsSlide.title'),
text: i18n.t('intro.aprilFoolsSlide.text'),
icon: 'fish',
view: () => <View/>,
mascotStyle: MASCOT_STYLE.NORMAL,
colors: ['#e01928', '#be1522'],
},
];
}
/**
* Render item to be used for the intro introSlides
*
* @param item The item to be displayed
* @param dimensions Dimensions of the item
*/
static getIntroRenderItem({item, dimensions}: Object) {
getIntroRenderItem = ({item, dimensions}: { item: Slide, dimensions: { width: number, height: number } }) => {
const index = parseInt(item.key);
return (
<LinearGradient
style={[
styles.mainContent,
dimensions,
dimensions
]}
colors={item.colors}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}
>
{item.image !== undefined ?
<View style={styles.image}>
<Image
source={item.image}
resizeMode={"contain"}
style={{width: '100%', height: '100%'}}
/>
</View>
: <MaterialCommunityIcons
name={item.icon}
color={'#fff'}
size={200}/>}
<View style={{marginTop: 20}}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.text}>{item.text}</Text>
</View>
{this.state.currentSlide === index
? <View style={{height: "100%", flex: 1}}>
<View style={{flex: 1}}>
{item.view()}
</View>
<Animatable.View
animation={"fadeIn"}>
{index !== 0
? <Animatable.View
animation={"pulse"}
iterationCount={"infinite"}
duration={2000}
style={{
marginLeft: 30,
marginBottom: 0,
width: 80
}}>
<Mascot emotion={item.mascotStyle} size={80}/>
</Animatable.View> : null}
<View style={{
marginLeft: 50,
width: 0,
height: 0,
borderLeftWidth: 20,
borderRightWidth: 0,
borderBottomWidth: 20,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: "rgba(0,0,0,0.60)",
}}/>
<Card style={{
backgroundColor: "rgba(0,0,0,0.38)",
marginHorizontal: 20,
borderColor: "rgba(0,0,0,0.60)",
borderWidth: 4,
borderRadius: 10,
}}>
<Card.Content>
<Animatable.Text
animation={"fadeIn"}
delay={100}
style={styles.title}>
{item.title}
</Animatable.Text>
<Animatable.Text
animation={"fadeIn"}
delay={200}
style={styles.text}>
{item.text}
</Animatable.Text>
</Card.Content>
</Card>
</Animatable.View>
</View> : null}
</LinearGradient>
);
}
getWelcomeView = () => {
return (
<View style={{flex: 1}}>
<View
style={styles.center}>
<Mascot
size={250}
emotion={MASCOT_STYLE.NORMAL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 2000,
}}
/>
</View>
</View>
)
}
getIconView(icon: MaterialCommunityIconsGlyphs) {
return (
<View style={{flex: 1}}>
<Animatable.View
style={styles.center}
animation={"fadeIn"}
>
<MaterialCommunityIcons
name={icon}
color={'#fff'}
size={200}/>
</Animatable.View>
</View>
)
}
setStatusBarColor(color: string) {
if (Platform.OS === 'android')
StatusBar.setBackgroundColor(color, true);
@ -154,12 +242,13 @@ export default class CustomIntroSlider extends React.Component<Props> {
onSlideChange = (index: number, lastIndex: number) => {
this.setStatusBarColor(this.currentSlides[index].colors[0]);
this.setState({currentSlide: index});
};
onSkip = () => {
this.setStatusBarColor(this.currentSlides[this.currentSlides.length-1].colors[0]);
this.setStatusBarColor(this.currentSlides[this.currentSlides.length - 1].colors[0]);
if (this.sliderRef.current != null)
this.sliderRef.current.goToSlide(this.currentSlides.length-1);
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
}
onDone = () => {
@ -167,6 +256,42 @@ export default class CustomIntroSlider extends React.Component<Props> {
this.props.onDone();
}
renderNextButton = () => {
return (
<Animatable.View
animation={"fadeIn"}
style={{
borderRadius: 25,
padding: 5,
backgroundColor: "rgba(0,0,0,0.2)"
}}>
<MaterialCommunityIcons
name={"arrow-right"}
color={'#fff'}
size={40}/>
</Animatable.View>
)
}
renderDoneButton = () => {
return (
<Animatable.View
animation={"bounceIn"}
style={{
borderRadius: 25,
padding: 5,
backgroundColor: "rgb(190,21,34)"
}}>
<MaterialCommunityIcons
name={"check"}
color={'#fff'}
size={40}/>
</Animatable.View>
)
}
render() {
this.currentSlides = this.introSlides;
if (this.props.isUpdate)
@ -177,16 +302,16 @@ export default class CustomIntroSlider extends React.Component<Props> {
return (
<AppIntroSlider
ref={this.sliderRef}
renderItem={CustomIntroSlider.getIntroRenderItem}
data={this.currentSlides}
extraData={this.state.currentSlide}
renderItem={this.getIntroRenderItem}
renderNextButton={this.renderNextButton}
renderDoneButton={this.renderDoneButton}
onDone={this.onDone}
bottomButton
showSkipButton
onSlideChange={this.onSlideChange}
onSkip={this.onSkip}
skipLabel={i18n.t('intro.buttons.skip')}
doneLabel={i18n.t('intro.buttons.done')}
nextLabel={i18n.t('intro.buttons.next')}
/>
);
}
@ -196,15 +321,7 @@ export default class CustomIntroSlider extends React.Component<Props> {
const styles = StyleSheet.create({
mainContent: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 100
},
image: {
width: 300,
height: 300,
marginBottom: -50,
paddingBottom: 100,
},
text: {
color: 'rgba(255, 255, 255, 0.8)',
@ -219,4 +336,10 @@ const styles = StyleSheet.create({
textAlign: 'center',
marginBottom: 16,
},
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
}
});