Compare commits
18 commits
547af66977
...
c86281cbd2
| Author | SHA1 | Date | |
|---|---|---|---|
| c86281cbd2 | |||
| 4cc9c61d72 | |||
| 1e81b2cd7b | |||
| cbe3777957 | |||
| 569e659779 | |||
| fcbc70956b | |||
| 3ce23726c2 | |||
| a3299c19f7 | |||
| 0a64f5fcd7 | |||
| 483970c9a8 | |||
| 3e4f2f4ac1 | |||
| 7107a8eadf | |||
| 7ac62b99f4 | |||
| aa992d20b2 | |||
| 0117b25cd8 | |||
| 4db4516296 | |||
| 7b94afadcc | |||
| 1cc0802c12 |
105 changed files with 10349 additions and 9360 deletions
4
App.js
4
App.js
|
|
@ -10,7 +10,7 @@ import {OverflowMenuProvider} from 'react-navigation-header-buttons';
|
|||
import LocaleManager from './src/managers/LocaleManager';
|
||||
import AsyncStorageManager from './src/managers/AsyncStorageManager';
|
||||
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
|
||||
import type {CustomTheme} from './src/managers/ThemeManager';
|
||||
import type {CustomThemeType} from './src/managers/ThemeManager';
|
||||
import ThemeManager from './src/managers/ThemeManager';
|
||||
import MainNavigator from './src/navigation/MainNavigator';
|
||||
import AprilFoolsManager from './src/managers/AprilFoolsManager';
|
||||
|
|
@ -35,7 +35,7 @@ type StateType = {
|
|||
showIntro: boolean,
|
||||
showUpdate: boolean,
|
||||
showAprilFools: boolean,
|
||||
currentTheme: CustomTheme | null,
|
||||
currentTheme: CustomThemeType | null,
|
||||
};
|
||||
|
||||
export default class App extends React.Component<null, StateType> {
|
||||
|
|
|
|||
|
|
@ -1,210 +1,217 @@
|
|||
jest.mock('react-native-keychain');
|
||||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import ConnectionManager from "../../src/managers/ConnectionManager";
|
||||
import {ERROR_TYPE} from "../../src/utils/WebData";
|
||||
import ConnectionManager from '../../src/managers/ConnectionManager';
|
||||
import {ERROR_TYPE} from '../../src/utils/WebData';
|
||||
|
||||
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
jest.mock('react-native-keychain');
|
||||
|
||||
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
const c = ConnectionManager.getInstance();
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('isLoggedIn yes', () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
return expect(c.isLoggedIn()).toBe(true);
|
||||
return expect(c.isLoggedIn()).toBe(true);
|
||||
});
|
||||
|
||||
test('isLoggedIn no', () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return null;
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
return expect(c.isLoggedIn()).toBe(false);
|
||||
return expect(c.isLoggedIn()).toBe(false);
|
||||
});
|
||||
|
||||
test("isConnectionResponseValid", () => {
|
||||
let json = {
|
||||
error: 0,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 2,
|
||||
data: {}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 0,
|
||||
data: {token: ''}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 'prout',
|
||||
data: {token: ''}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeFalse();
|
||||
test('connect bad credentials', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_CREDENTIALS,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.BAD_CREDENTIALS,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect bad credentials", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_CREDENTIALS,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
|
||||
});
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'saveLogin')
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
test("connect good credentials", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials no consent', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.NO_CONSENT,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.NO_CONSENT,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect good credentials no consent", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.NO_CONSENT,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials, fail save token', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.NO_CONSENT);
|
||||
});
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'saveLogin')
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.reject(false);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.UNKNOWN,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect good credentials, fail save token", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
|
||||
return Promise.reject(false);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.UNKNOWN);
|
||||
test('connect connection error', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.CONNECTION_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect connection error", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
test('connect bogus response 1', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
thing: true,
|
||||
wrong: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.CONNECTION_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect bogus response 1", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
thing: true,
|
||||
wrong: '',
|
||||
}
|
||||
},
|
||||
})
|
||||
test('authenticatedRequest success', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {coucou: 'toi'},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).resolves.toStrictEqual({coucou: 'toi'});
|
||||
});
|
||||
|
||||
|
||||
test("authenticatedRequest success", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error wrong token', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {coucou: 'toi'}
|
||||
};
|
||||
},
|
||||
})
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_TOKEN,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.resolves.toStrictEqual({coucou: 'toi'});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error wrong token", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error bogus response', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_TOKEN,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error bogus response", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest connection error', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest connection error", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error no token', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject()
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error no token", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.UNKNOWN);
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.UNKNOWN);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,319 +1,345 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as EquipmentBooking from "../../src/utils/EquipmentBooking";
|
||||
import i18n from "i18n-js";
|
||||
import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
test('getISODate', () => {
|
||||
let date = new Date("2020-03-05 12:00");
|
||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
|
||||
date = new Date("2020-03-05");
|
||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
|
||||
date = new Date("2020-03-05 00:00"); // Treated as local time
|
||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC
|
||||
let date = new Date('2020-03-05 12:00');
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||
date = new Date('2020-03-05');
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||
date = new Date('2020-03-05 00:00'); // Treated as local time
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-04'); // Treated as UTC
|
||||
});
|
||||
|
||||
test('getCurrentDay', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14 14:50:35').getTime()
|
||||
);
|
||||
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
|
||||
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
|
||||
new Date('2020-01-14').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
test('isEquipmentAvailable', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-07-09').getTime()
|
||||
);
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: "Petit barbecue",
|
||||
caution: 100,
|
||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
||||
};
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-09", end: "2020-07-10"}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-07", end: "2020-07-8"},
|
||||
{begin: "2020-07-10", end: "2020-07-12"},
|
||||
];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-8'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
||||
});
|
||||
|
||||
test('getFirstEquipmentAvailability', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-07-09').getTime()
|
||||
);
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: "Petit barbecue",
|
||||
caution: 100,
|
||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
||||
};
|
||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime());
|
||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
|
||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-07", end: "2020-07-09"},
|
||||
{begin: "2020-07-10", end: "2020-07-16"},
|
||||
];
|
||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-07", end: "2020-07-09"},
|
||||
{begin: "2020-07-10", end: "2020-07-12"},
|
||||
{begin: "2020-07-14", end: "2020-07-16"},
|
||||
];
|
||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-11').getTime());
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-10').getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-16'},
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-17').getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
{begin: '2020-07-14', end: '2020-07-16'},
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-13').getTime());
|
||||
});
|
||||
|
||||
test('getRelativeDateString', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-07-09').getTime()
|
||||
);
|
||||
jest.spyOn(i18n, 't')
|
||||
.mockImplementation((translationString: string) => {
|
||||
const prefix = "screens.equipment.";
|
||||
if (translationString === prefix + "otherYear")
|
||||
return "0";
|
||||
else if (translationString === prefix + "otherMonth")
|
||||
return "1";
|
||||
else if (translationString === prefix + "thisMonth")
|
||||
return "2";
|
||||
else if (translationString === prefix + "tomorrow")
|
||||
return "3";
|
||||
else if (translationString === prefix + "today")
|
||||
return "4";
|
||||
else
|
||||
return null;
|
||||
}
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-10"))).toBe("3");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-11"))).toBe("2");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-30"))).toBe("2");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-08-30"))).toBe("1");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2020-11-10"))).toBe("1");
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date("2021-11-10"))).toBe("0");
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
|
||||
const prefix = 'screens.equipment.';
|
||||
if (translationString === prefix + 'otherYear') return '0';
|
||||
else if (translationString === prefix + 'otherMonth') return '1';
|
||||
else if (translationString === prefix + 'thisMonth') return '2';
|
||||
else if (translationString === prefix + 'tomorrow') return '3';
|
||||
else if (translationString === prefix + 'today') return '4';
|
||||
else return null;
|
||||
});
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe(
|
||||
'4',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe(
|
||||
'3',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe(
|
||||
'2',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe(
|
||||
'2',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe(
|
||||
'1',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe(
|
||||
'1',
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe(
|
||||
'0',
|
||||
);
|
||||
});
|
||||
|
||||
test('getValidRange', () => {
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: "Petit barbecue",
|
||||
caution: 100,
|
||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
||||
};
|
||||
let start = new Date("2020-07-11");
|
||||
let end = new Date("2020-07-15");
|
||||
let result = [
|
||||
"2020-07-11",
|
||||
"2020-07-12",
|
||||
"2020-07-13",
|
||||
"2020-07-14",
|
||||
"2020-07-15",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-07", end: "2020-07-10"},
|
||||
{begin: "2020-07-13", end: "2020-07-15"},
|
||||
];
|
||||
result = [
|
||||
"2020-07-11",
|
||||
"2020-07-12",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-15');
|
||||
let result = [
|
||||
'2020-07-11',
|
||||
'2020-07-12',
|
||||
'2020-07-13',
|
||||
'2020-07-14',
|
||||
'2020-07-15',
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-10'},
|
||||
{begin: '2020-07-13', end: '2020-07-15'},
|
||||
];
|
||||
result = ['2020-07-11', '2020-07-12'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"}];
|
||||
result = ["2020-07-11"];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
|
||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},];
|
||||
result = [
|
||||
"2020-07-13",
|
||||
"2020-07-14",
|
||||
"2020-07-15",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
||||
start = new Date("2020-07-14");
|
||||
end = new Date("2020-07-14");
|
||||
result = [
|
||||
"2020-07-14",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result);
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
result = ['2020-07-11'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
|
||||
result = ['2020-07-13', '2020-07-14', '2020-07-15'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-14');
|
||||
result = ['2020-07-14'];
|
||||
expect(
|
||||
EquipmentBooking.getValidRange(start, start, testDevice),
|
||||
).toStrictEqual(result);
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
start = new Date("2020-07-14");
|
||||
end = new Date("2020-07-17");
|
||||
result = [
|
||||
"2020-07-14",
|
||||
"2020-07-15",
|
||||
"2020-07-16",
|
||||
"2020-07-17",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-17');
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}];
|
||||
result = [
|
||||
"2020-07-14",
|
||||
"2020-07-15",
|
||||
"2020-07-16",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
|
||||
testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-12", end: "2020-07-13"},
|
||||
{begin: "2020-07-15", end: "2020-07-20"},
|
||||
];
|
||||
start = new Date("2020-07-11");
|
||||
end = new Date("2020-07-23");
|
||||
result = [
|
||||
"2020-07-21",
|
||||
"2020-07-22",
|
||||
"2020-07-23",
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
result = ['2020-07-21', '2020-07-22', '2020-07-23'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
});
|
||||
|
||||
test('generateMarkedDates', () => {
|
||||
let theme = {
|
||||
colors: {
|
||||
primary: "primary",
|
||||
danger: "primary",
|
||||
textDisabled: "primary",
|
||||
}
|
||||
}
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: "Petit barbecue",
|
||||
caution: 100,
|
||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
||||
};
|
||||
let start = new Date("2020-07-11");
|
||||
let end = new Date("2020-07-13");
|
||||
let range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
let result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
"2020-07-12": {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger
|
||||
},
|
||||
"2020-07-13": {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
};
|
||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
||||
result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
"2020-07-12": {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
"2020-07-13": {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
};
|
||||
expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
|
||||
result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
"2020-07-12": {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
"2020-07-13": {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
|
||||
let theme = {
|
||||
colors: {
|
||||
primary: 'primary',
|
||||
danger: 'primary',
|
||||
textDisabled: 'primary',
|
||||
},
|
||||
};
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-13');
|
||||
let range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
let result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-13", end: "2020-07-15"},];
|
||||
result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
"2020-07-12": {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
||||
testDevice.booked_at = [{begin: '2020-07-13', end: '2020-07-15'}];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"},];
|
||||
result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: "2020-07-12", end: "2020-07-13"},
|
||||
{begin: "2020-07-15", end: "2020-07-20"},
|
||||
];
|
||||
start = new Date("2020-07-11");
|
||||
end = new Date("2020-07-23");
|
||||
result = {
|
||||
"2020-07-11": {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
result = {
|
||||
"2020-07-21": {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
"2020-07-22": {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger
|
||||
},
|
||||
"2020-07-23": {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-21': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-22': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger,
|
||||
},
|
||||
'2020-07-23': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,210 +1,222 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as Planning from "../../src/utils/Planning";
|
||||
import * as Planning from '../../src/utils/Planning';
|
||||
|
||||
test('isDescriptionEmpty', () => {
|
||||
expect(Planning.isDescriptionEmpty("")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(" ")).toBeTrue();
|
||||
// noinspection CheckTagEmptyBody
|
||||
expect(Planning.isDescriptionEmpty("<p></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p> </p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br></p><p><br></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("coucou")).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty("<p>coucou</p>")).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty('')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(' ')).toBeTrue();
|
||||
// noinspection CheckTagEmptyBody
|
||||
expect(Planning.isDescriptionEmpty('<p></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p> </p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br></p><p><br></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br><br><br></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('coucou')).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty('<p>coucou</p>')).toBeFalse();
|
||||
});
|
||||
|
||||
test('isEventDateStringFormatValid', () => {
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16")).toBeTrue();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21 09:00')).toBeTrue();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-64-12 01:16')).toBeTrue();
|
||||
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
|
||||
).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'),
|
||||
).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
|
||||
});
|
||||
|
||||
test('stringToDate', () => {
|
||||
let testDate = new Date();
|
||||
expect(Planning.stringToDate(undefined)).toBeNull();
|
||||
expect(Planning.stringToDate("")).toBeNull();
|
||||
expect(Planning.stringToDate("garbage")).toBeNull();
|
||||
expect(Planning.stringToDate("2020-03-21")).toBeNull();
|
||||
expect(Planning.stringToDate("09:00:00")).toBeNull();
|
||||
expect(Planning.stringToDate("2020-03-21 09:g0")).toBeNull();
|
||||
expect(Planning.stringToDate("2020-03-21 09:g0:")).toBeNull();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.stringToDate("2020-03-21 09:00")).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 0, 31);
|
||||
testDate.setHours(18, 30, 0, 0);
|
||||
expect(Planning.stringToDate("2020-01-31 18:30")).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 50, 50);
|
||||
testDate.setHours(65, 65, 0, 0);
|
||||
expect(Planning.stringToDate("2020-51-50 65:65")).toEqual(testDate);
|
||||
let testDate = new Date();
|
||||
expect(Planning.stringToDate(undefined)).toBeNull();
|
||||
expect(Planning.stringToDate('')).toBeNull();
|
||||
expect(Planning.stringToDate('garbage')).toBeNull();
|
||||
expect(Planning.stringToDate('2020-03-21')).toBeNull();
|
||||
expect(Planning.stringToDate('09:00:00')).toBeNull();
|
||||
expect(Planning.stringToDate('2020-03-21 09:g0')).toBeNull();
|
||||
expect(Planning.stringToDate('2020-03-21 09:g0:')).toBeNull();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.stringToDate('2020-03-21 09:00')).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 0, 31);
|
||||
testDate.setHours(18, 30, 0, 0);
|
||||
expect(Planning.stringToDate('2020-01-31 18:30')).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 50, 50);
|
||||
testDate.setHours(65, 65, 0, 0);
|
||||
expect(Planning.stringToDate('2020-51-50 65:65')).toEqual(testDate);
|
||||
});
|
||||
|
||||
test('getFormattedEventTime', () => {
|
||||
expect(Planning.getFormattedEventTime(null, null))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(undefined, undefined))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime("20:30", "23:00"))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31"))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(null, null)).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe(
|
||||
'/ - /',
|
||||
);
|
||||
|
||||
|
||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00"))
|
||||
.toBe('09:00');
|
||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00"))
|
||||
.toBe('09:00 - 23:59');
|
||||
expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00"))
|
||||
.toBe('20:30 - 23:00');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00'),
|
||||
).toBe('09:00');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'),
|
||||
).toBe('09:00 - 23:59');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
|
||||
).toBe('20:30 - 23:00');
|
||||
});
|
||||
|
||||
test('getDateOnlyString', () => {
|
||||
expect(Planning.getDateOnlyString("2020-03-21 09:00")).toBe("2020-03-21");
|
||||
expect(Planning.getDateOnlyString("2021-12-15 09:00")).toBe("2021-12-15");
|
||||
expect(Planning.getDateOnlyString("2021-12-o5 09:00")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("2021-12-15")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("garbage")).toBeNull();
|
||||
expect(Planning.getDateOnlyString('2020-03-21 09:00')).toBe('2020-03-21');
|
||||
expect(Planning.getDateOnlyString('2021-12-15 09:00')).toBe('2021-12-15');
|
||||
expect(Planning.getDateOnlyString('2021-12-o5 09:00')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('2021-12-15 09:')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('2021-12-15')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('garbage')).toBeNull();
|
||||
});
|
||||
|
||||
test('isEventBefore', () => {
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-05-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-30 10:15'),
|
||||
).toBeTrue();
|
||||
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-05-21 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
|
||||
expect(Planning.isEventBefore(
|
||||
"garbage", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
undefined, undefined)).toBeFalse();
|
||||
expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
|
||||
expect(Planning.isEventBefore(undefined, undefined)).toBeFalse();
|
||||
});
|
||||
|
||||
test('dateToString', () => {
|
||||
let testDate = new Date();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00");
|
||||
testDate.setFullYear(2021, 0, 12);
|
||||
testDate.setHours(9, 10, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10");
|
||||
testDate.setFullYear(2022, 11, 31);
|
||||
testDate.setHours(9, 10, 15, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10");
|
||||
let testDate = new Date();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2020-03-21 09:00');
|
||||
testDate.setFullYear(2021, 0, 12);
|
||||
testDate.setHours(9, 10, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2021-01-12 09:10');
|
||||
testDate.setFullYear(2022, 11, 31);
|
||||
testDate.setHours(9, 10, 15, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2022-12-31 09:10');
|
||||
});
|
||||
|
||||
test('generateEmptyCalendar', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
||||
);
|
||||
let calendar = Planning.generateEmptyCalendar(1);
|
||||
expect(calendar).toHaveProperty("2020-01-14");
|
||||
expect(calendar).toHaveProperty("2020-01-20");
|
||||
expect(calendar).toHaveProperty("2020-02-10");
|
||||
expect(Object.keys(calendar).length).toBe(32);
|
||||
calendar = Planning.generateEmptyCalendar(3);
|
||||
expect(calendar).toHaveProperty("2020-01-14");
|
||||
expect(calendar).toHaveProperty("2020-01-20");
|
||||
expect(calendar).toHaveProperty("2020-02-10");
|
||||
expect(calendar).toHaveProperty("2020-02-14");
|
||||
expect(calendar).toHaveProperty("2020-03-20");
|
||||
expect(calendar).toHaveProperty("2020-04-12");
|
||||
expect(Object.keys(calendar).length).toBe(92);
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||
let calendar = Planning.generateEmptyCalendar(1);
|
||||
expect(calendar).toHaveProperty('2020-01-14');
|
||||
expect(calendar).toHaveProperty('2020-01-20');
|
||||
expect(calendar).toHaveProperty('2020-02-10');
|
||||
expect(Object.keys(calendar).length).toBe(32);
|
||||
calendar = Planning.generateEmptyCalendar(3);
|
||||
expect(calendar).toHaveProperty('2020-01-14');
|
||||
expect(calendar).toHaveProperty('2020-01-20');
|
||||
expect(calendar).toHaveProperty('2020-02-10');
|
||||
expect(calendar).toHaveProperty('2020-02-14');
|
||||
expect(calendar).toHaveProperty('2020-03-20');
|
||||
expect(calendar).toHaveProperty('2020-04-12');
|
||||
expect(Object.keys(calendar).length).toBe(92);
|
||||
});
|
||||
|
||||
test('pushEventInOrder', () => {
|
||||
let eventArray = [];
|
||||
let event1 = {date_begin: "2020-01-14 09:15"};
|
||||
Planning.pushEventInOrder(eventArray, event1);
|
||||
expect(eventArray.length).toBe(1);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
let eventArray = [];
|
||||
let event1 = {date_begin: '2020-01-14 09:15'};
|
||||
Planning.pushEventInOrder(eventArray, event1);
|
||||
expect(eventArray.length).toBe(1);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
|
||||
let event2 = {date_begin: "2020-01-14 10:15"};
|
||||
Planning.pushEventInOrder(eventArray, event2);
|
||||
expect(eventArray.length).toBe(2);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
let event2 = {date_begin: '2020-01-14 10:15'};
|
||||
Planning.pushEventInOrder(eventArray, event2);
|
||||
expect(eventArray.length).toBe(2);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
|
||||
let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"};
|
||||
Planning.pushEventInOrder(eventArray, event3);
|
||||
expect(eventArray.length).toBe(3);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
expect(eventArray[2]).toBe(event3);
|
||||
let event3 = {date_begin: '2020-01-14 10:15', title: 'garbage'};
|
||||
Planning.pushEventInOrder(eventArray, event3);
|
||||
expect(eventArray.length).toBe(3);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
expect(eventArray[2]).toBe(event3);
|
||||
|
||||
let event4 = {date_begin: "2020-01-13 09:00"};
|
||||
Planning.pushEventInOrder(eventArray, event4);
|
||||
expect(eventArray.length).toBe(4);
|
||||
expect(eventArray[0]).toBe(event4);
|
||||
expect(eventArray[1]).toBe(event1);
|
||||
expect(eventArray[2]).toBe(event2);
|
||||
expect(eventArray[3]).toBe(event3);
|
||||
let event4 = {date_begin: '2020-01-13 09:00'};
|
||||
Planning.pushEventInOrder(eventArray, event4);
|
||||
expect(eventArray.length).toBe(4);
|
||||
expect(eventArray[0]).toBe(event4);
|
||||
expect(eventArray[1]).toBe(event1);
|
||||
expect(eventArray[2]).toBe(event2);
|
||||
expect(eventArray[3]).toBe(event3);
|
||||
});
|
||||
|
||||
test('generateEventAgenda', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
||||
);
|
||||
let eventList = [
|
||||
{date_begin: "2020-01-14 09:15"},
|
||||
{date_begin: "2020-02-01 09:15"},
|
||||
{date_begin: "2020-01-15 09:15"},
|
||||
{date_begin: "2020-02-01 09:30"},
|
||||
{date_begin: "2020-02-01 08:30"},
|
||||
];
|
||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||
expect(calendar["2020-01-14"].length).toBe(1);
|
||||
expect(calendar["2020-01-14"][0]).toBe(eventList[0]);
|
||||
expect(calendar["2020-01-15"].length).toBe(1);
|
||||
expect(calendar["2020-01-15"][0]).toBe(eventList[2]);
|
||||
expect(calendar["2020-02-01"].length).toBe(3);
|
||||
expect(calendar["2020-02-01"][0]).toBe(eventList[4]);
|
||||
expect(calendar["2020-02-01"][1]).toBe(eventList[1]);
|
||||
expect(calendar["2020-02-01"][2]).toBe(eventList[3]);
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||
let eventList = [
|
||||
{date_begin: '2020-01-14 09:15'},
|
||||
{date_begin: '2020-02-01 09:15'},
|
||||
{date_begin: '2020-01-15 09:15'},
|
||||
{date_begin: '2020-02-01 09:30'},
|
||||
{date_begin: '2020-02-01 08:30'},
|
||||
];
|
||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||
expect(calendar['2020-01-14'].length).toBe(1);
|
||||
expect(calendar['2020-01-14'][0]).toBe(eventList[0]);
|
||||
expect(calendar['2020-01-15'].length).toBe(1);
|
||||
expect(calendar['2020-01-15'][0]).toBe(eventList[2]);
|
||||
expect(calendar['2020-02-01'].length).toBe(3);
|
||||
expect(calendar['2020-02-01'][0]).toBe(eventList[4]);
|
||||
expect(calendar['2020-02-01'][1]).toBe(eventList[1]);
|
||||
expect(calendar['2020-02-01'][2]).toBe(eventList[3]);
|
||||
});
|
||||
|
||||
test('getCurrentDateString', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() => {
|
||||
let date = new Date();
|
||||
date.setFullYear(2020, 0, 14);
|
||||
date.setHours(15, 30, 54, 65);
|
||||
return date.getTime();
|
||||
});
|
||||
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => {
|
||||
let date = new Date();
|
||||
date.setFullYear(2020, 0, 14);
|
||||
date.setHours(15, 30, 54, 65);
|
||||
return date.getTime();
|
||||
});
|
||||
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,142 +1,167 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash";
|
||||
import {
|
||||
getCleanedMachineWatched,
|
||||
getMachineEndDate,
|
||||
getMachineOfId,
|
||||
isMachineWatched,
|
||||
} from '../../src/utils/Proxiwash';
|
||||
|
||||
test('getMachineEndDate', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T15:00:00.000Z').getTime()
|
||||
);
|
||||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||
expectDate.setHours(23);
|
||||
expectDate.setMinutes(10);
|
||||
expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T15:00:00.000Z').getTime());
|
||||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||
expectDate.setHours(23);
|
||||
expectDate.setMinutes(10);
|
||||
expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
|
||||
expectDate.setHours(16);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: "16:30"}).getTime()).toBe(expectDate.getTime());
|
||||
expectDate.setHours(16);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
|
||||
expect(getMachineEndDate({endTime: "15:30"})).toBeNull();
|
||||
expect(getMachineEndDate({endTime: '15:30'})).toBeNull();
|
||||
|
||||
expect(getMachineEndDate({endTime: "13:10"})).toBeNull();
|
||||
expect(getMachineEndDate({endTime: '13:10'})).toBeNull();
|
||||
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T23:00:00.000Z').getTime()
|
||||
);
|
||||
expectDate = new Date('2020-01-14T23:00:00.000Z');
|
||||
expectDate.setHours(0);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: "00:30"}).getTime()).toBe(expectDate.getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T23:00:00.000Z').getTime());
|
||||
expectDate = new Date('2020-01-14T23:00:00.000Z');
|
||||
expectDate.setHours(0);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
test('isMachineWatched', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
];
|
||||
expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue();
|
||||
expect(isMachineWatched({number: "1", endTime: "20:30"}, machineList)).toBeTrue();
|
||||
expect(isMachineWatched({number: "3", endTime: "20:30"}, machineList)).toBeFalse();
|
||||
expect(isMachineWatched({number: "1", endTime: "23:30"}, machineList)).toBeFalse();
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
];
|
||||
expect(
|
||||
isMachineWatched({number: '0', endTime: '23:30'}, machineList),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
isMachineWatched({number: '1', endTime: '20:30'}, machineList),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
isMachineWatched({number: '3', endTime: '20:30'}, machineList),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
isMachineWatched({number: '1', endTime: '23:30'}, machineList),
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
test('getMachineOfId', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
},
|
||||
];
|
||||
expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"});
|
||||
expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"});
|
||||
expect(getMachineOfId("3", machineList)).toBeNull();
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
},
|
||||
];
|
||||
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
|
||||
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
|
||||
expect(getMachineOfId('3', machineList)).toBeNull();
|
||||
});
|
||||
|
||||
test('getCleanedMachineWatched', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "",
|
||||
},
|
||||
];
|
||||
let watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "",
|
||||
},
|
||||
];
|
||||
let cleanedList = watchList;
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '',
|
||||
},
|
||||
];
|
||||
let watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '',
|
||||
},
|
||||
];
|
||||
let cleanedList = watchList;
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
);
|
||||
|
||||
watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "15:30",
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '15:30',
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
);
|
||||
|
||||
watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:31",
|
||||
},
|
||||
{
|
||||
number: "3",
|
||||
endTime: "15:30",
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
});
|
||||
watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:31',
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
endTime: '15:30',
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import React from 'react';
|
||||
import {isResponseValid} from "../../src/utils/WebData";
|
||||
|
||||
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
test('isRequestResponseValid', () => {
|
||||
let json = {
|
||||
error: 0,
|
||||
data: {}
|
||||
};
|
||||
expect(isResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 1,
|
||||
data: {}
|
||||
};
|
||||
expect(isResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 50,
|
||||
data: {}
|
||||
};
|
||||
expect(isResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 50,
|
||||
data: {truc: 'machin'}
|
||||
};
|
||||
expect(isResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
message: 'coucou'
|
||||
};
|
||||
expect(isResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 'coucou',
|
||||
data: {truc: 'machin'}
|
||||
};
|
||||
expect(isResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 0,
|
||||
data: 'coucou'
|
||||
};
|
||||
expect(isResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 0,
|
||||
};
|
||||
expect(isResponseValid(json)).toBeFalse();
|
||||
});
|
||||
47
__tests__/utils/WebData.test.js
Normal file
47
__tests__/utils/WebData.test.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import {isApiResponseValid} from '../../src/utils/WebData';
|
||||
|
||||
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
test('isRequestResponseValid', () => {
|
||||
let json = {
|
||||
error: 0,
|
||||
data: {},
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 1,
|
||||
data: {},
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 50,
|
||||
data: {},
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 50,
|
||||
data: {truc: 'machin'},
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
message: 'coucou',
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 'coucou',
|
||||
data: {truc: 'machin'},
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 0,
|
||||
data: 'coucou',
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 0,
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
});
|
||||
1
index.js
1
index.js
|
|
@ -6,4 +6,5 @@ import {AppRegistry} from 'react-native';
|
|||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
transformer: {
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import * as React from 'react';
|
|||
import {View} from 'react-native';
|
||||
import {Headline, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class VoteNotAvailable extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import {
|
|||
import {FlatList, StyleSheet} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
teams: Array<VoteTeamType>,
|
||||
dateEnd: string,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import {
|
|||
} from 'react-native-paper';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
startDate: string | null,
|
||||
justVoted: boolean,
|
||||
hasVoted: boolean,
|
||||
isVoteRunning: boolean,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import {View} from 'react-native';
|
|||
import {List, withTheme} from 'react-native-paper';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
left?: () => React.Node,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import * as Animatable from 'react-native-animatable';
|
|||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {OnScrollType} from '../../utils/AutoHideHandler';
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
onPress: (action: string, data?: string) => void,
|
||||
seekAttention: boolean,
|
||||
};
|
||||
|
|
@ -94,7 +95,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
onScroll = (event: OnScrollType) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import {withCollapsible} from '../../utils/withCollapsible';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
|
||||
export type CollapsibleComponentPropsType = {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import {List, withTheme} from 'react-native-paper';
|
|||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class ActionsDashBoardItem extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import {
|
|||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
eventNumber: number,
|
||||
clickAction: () => void,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import * as React from 'react';
|
|||
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
image: string | null,
|
||||
onPress: () => void | null,
|
||||
badgeCount: number | null,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import type {
|
|||
ClubCategoryType,
|
||||
ClubType,
|
||||
} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
onPress: () => void,
|
||||
categoryTranslator: (id: number) => ClubCategoryType,
|
||||
item: ClubType,
|
||||
height: number,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class ClubListItem extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import type {
|
|||
ServiceCategoryType,
|
||||
ServiceItemType,
|
||||
} from '../../../managers/ServicesManager';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceCategoryType,
|
||||
activeDashboard: Array<string>,
|
||||
onPress: (service: ServiceItemType) => void,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import * as React from 'react';
|
||||
import {Image} from 'react-native';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
|
||||
type PropsType = {
|
||||
|
|
@ -11,7 +11,7 @@ type PropsType = {
|
|||
isActive: boolean,
|
||||
height: number,
|
||||
onPress: () => void,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class DashboardEditItem extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
import * as React from 'react';
|
||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
image: string,
|
||||
isActive: boolean,
|
||||
onPress: () => void,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
|||
import {Avatar, List, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import {
|
||||
getFirstEquipmentAvailability,
|
||||
|
|
@ -17,7 +17,7 @@ type PropsType = {
|
|||
userDeviceRentDates: [string, string],
|
||||
item: DeviceType,
|
||||
height: number,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class EquipmentListItem extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
PlanexGroupType,
|
||||
PlanexGroupCategoryType,
|
||||
} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
item: PlanexGroupCategoryType,
|
||||
|
|
@ -19,7 +19,7 @@ type PropsType = {
|
|||
currentSearchString: string,
|
||||
favoriteNumber: number,
|
||||
height: number,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {IconButton, List, withTheme} from 'react-native-paper';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
|
||||
type PropsType = {
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
onPress: () => void,
|
||||
onStarPress: () => void,
|
||||
item: PlanexGroupType,
|
||||
|
|
|
|||
|
|
@ -1,194 +1,236 @@
|
|||
import * as React from 'react';
|
||||
import {Avatar, Caption, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import ProxiwashConstants from "../../../constants/ProxiwashConstants";
|
||||
import i18n from "i18n-js";
|
||||
import AprilFoolsManager from "../../../managers/AprilFoolsManager";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {Machine} from "../../../screens/Proxiwash/ProxiwashScreen";
|
||||
// @flow
|
||||
|
||||
type Props = {
|
||||
item: Machine,
|
||||
theme: CustomTheme,
|
||||
onPress: Function,
|
||||
isWatched: boolean,
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Caption,
|
||||
List,
|
||||
ProgressBar,
|
||||
Surface,
|
||||
Text,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
|
||||
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||
|
||||
type PropsType = {
|
||||
item: ProxiwashMachineType,
|
||||
theme: CustomThemeType,
|
||||
onPress: (
|
||||
title: string,
|
||||
item: ProxiwashMachineType,
|
||||
isDryer: boolean,
|
||||
height: number,
|
||||
}
|
||||
) => void,
|
||||
isWatched: boolean,
|
||||
isDryer: boolean,
|
||||
height: number,
|
||||
};
|
||||
|
||||
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 5,
|
||||
justifyContent: 'center',
|
||||
elevation: 1,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
progressBar: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a proxiwash item, showing machine progression and state
|
||||
*/
|
||||
class ProxiwashListItem extends React.Component<Props> {
|
||||
class ProxiwashListItem extends React.Component<PropsType> {
|
||||
stateColors: {[key: string]: string};
|
||||
|
||||
stateColors: Object;
|
||||
stateStrings: Object;
|
||||
stateStrings: {[key: string]: string};
|
||||
|
||||
title: string;
|
||||
title: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.stateColors = {};
|
||||
this.stateStrings = {};
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.stateColors = {};
|
||||
this.stateStrings = {};
|
||||
|
||||
this.updateStateStrings();
|
||||
this.updateStateStrings();
|
||||
|
||||
let displayNumber = props.item.number;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number));
|
||||
let displayNumber = props.item.number;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
|
||||
parseInt(props.item.number, 10),
|
||||
);
|
||||
|
||||
this.title = props.isDryer
|
||||
? i18n.t('screens.proxiwash.dryer')
|
||||
: i18n.t('screens.proxiwash.washer');
|
||||
this.title += ' n°' + displayNumber;
|
||||
}
|
||||
this.title = props.isDryer
|
||||
? i18n.t('screens.proxiwash.dryer')
|
||||
: i18n.t('screens.proxiwash.washer');
|
||||
this.title += ` n°${displayNumber}`;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
const props = this.props;
|
||||
return (nextProps.theme.dark !== props.theme.dark)
|
||||
|| (nextProps.item.state !== props.item.state)
|
||||
|| (nextProps.item.donePercent !== props.item.donePercent)
|
||||
|| (nextProps.isWatched !== props.isWatched);
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.item.state !== props.item.state ||
|
||||
nextProps.item.donePercent !== props.item.donePercent ||
|
||||
nextProps.isWatched !== props.isWatched
|
||||
);
|
||||
}
|
||||
|
||||
updateStateStrings() {
|
||||
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.states.ready');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.states.running');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.states.runningNotStarted');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.states.finished');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.states.broken');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.states.error');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.states.unknown');
|
||||
}
|
||||
onListItemPress = () => {
|
||||
const {props} = this;
|
||||
props.onPress(this.title, props.item, props.isDryer);
|
||||
};
|
||||
|
||||
updateStateColors() {
|
||||
const colors = this.props.theme.colors;
|
||||
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = colors.proxiwashReadyColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING] = colors.proxiwashRunningColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = colors.proxiwashRunningNotStartedColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.FINISHED] = colors.proxiwashFinishedColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.ERROR] = colors.proxiwashErrorColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] = colors.proxiwashUnknownColor;
|
||||
}
|
||||
updateStateStrings() {
|
||||
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
|
||||
'screens.proxiwash.states.ready',
|
||||
);
|
||||
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
|
||||
'screens.proxiwash.states.running',
|
||||
);
|
||||
this.stateStrings[
|
||||
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
|
||||
] = i18n.t('screens.proxiwash.states.runningNotStarted');
|
||||
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
|
||||
'screens.proxiwash.states.finished',
|
||||
);
|
||||
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
|
||||
'screens.proxiwash.states.broken',
|
||||
);
|
||||
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
|
||||
'screens.proxiwash.states.error',
|
||||
);
|
||||
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
|
||||
'screens.proxiwash.states.unknown',
|
||||
);
|
||||
}
|
||||
|
||||
onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer);
|
||||
updateStateColors() {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] =
|
||||
colors.proxiwashReadyColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING] =
|
||||
colors.proxiwashRunningColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] =
|
||||
colors.proxiwashRunningNotStartedColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.FINISHED] =
|
||||
colors.proxiwashFinishedColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] =
|
||||
colors.proxiwashBrokenColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.ERROR] =
|
||||
colors.proxiwashErrorColor;
|
||||
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] =
|
||||
colors.proxiwashUnknownColor;
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const colors = props.theme.colors;
|
||||
const machineState = props.item.state;
|
||||
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
|
||||
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
|
||||
const description = isRunning ? props.item.startTime + '/' + props.item.endTime : '';
|
||||
const stateIcon = ProxiwashConstants.stateIcons[machineState];
|
||||
const stateString = this.stateStrings[machineState];
|
||||
const progress = isRunning
|
||||
? props.item.donePercent !== ''
|
||||
? parseFloat(props.item.donePercent) / 100
|
||||
: 0
|
||||
: 1;
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
const machineState = props.item.state;
|
||||
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
|
||||
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
|
||||
const description = isRunning
|
||||
? `${props.item.startTime}/${props.item.endTime}`
|
||||
: '';
|
||||
const stateIcon = ProxiwashConstants.stateIcons[machineState];
|
||||
const stateString = this.stateStrings[machineState];
|
||||
let progress;
|
||||
if (isRunning && props.item.donePercent !== '')
|
||||
progress = parseFloat(props.item.donePercent) / 100;
|
||||
else if (isRunning) progress = 0;
|
||||
else progress = 1;
|
||||
|
||||
const icon = props.isWatched
|
||||
? <AnimatedIcon
|
||||
icon={'bell-ring'}
|
||||
animation={"rubberBand"}
|
||||
useNativeDriver
|
||||
size={50}
|
||||
color={colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
: <AnimatedIcon
|
||||
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
animation={isRunning ? "pulse" : undefined}
|
||||
iterationCount={"infinite"}
|
||||
easing={"linear"}
|
||||
duration={1000}
|
||||
useNativeDriver
|
||||
size={40}
|
||||
color={colors.text}
|
||||
style={styles.icon}
|
||||
/>;
|
||||
this.updateStateColors();
|
||||
return (
|
||||
<Surface
|
||||
style={{
|
||||
...styles.container,
|
||||
height: props.height,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{
|
||||
!isReady
|
||||
? <ProgressBar
|
||||
style={{
|
||||
...styles.progressBar,
|
||||
height: props.height
|
||||
}}
|
||||
progress={progress}
|
||||
color={this.stateColors[machineState]}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
<List.Item
|
||||
title={this.title}
|
||||
description={description}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={this.onListItemPress}
|
||||
left={() => icon}
|
||||
right={() => (
|
||||
<View style={{flexDirection: 'row',}}>
|
||||
<View style={{justifyContent: 'center',}}>
|
||||
<Text style={
|
||||
machineState === ProxiwashConstants.machineStates.FINISHED ?
|
||||
{fontWeight: 'bold',} : {}
|
||||
}
|
||||
>
|
||||
{stateString}
|
||||
</Text>
|
||||
{
|
||||
machineState === ProxiwashConstants.machineStates.RUNNING
|
||||
? <Caption>{props.item.remainingTime} min</Caption>
|
||||
: null
|
||||
}
|
||||
|
||||
</View>
|
||||
<View style={{justifyContent: 'center',}}>
|
||||
<Avatar.Icon
|
||||
icon={stateIcon}
|
||||
color={colors.text}
|
||||
size={30}
|
||||
style={styles.icon}
|
||||
/>
|
||||
</View>
|
||||
</View>)}
|
||||
const icon = props.isWatched ? (
|
||||
<AnimatedIcon
|
||||
icon="bell-ring"
|
||||
animation="rubberBand"
|
||||
useNativeDriver
|
||||
size={50}
|
||||
color={colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
) : (
|
||||
<AnimatedIcon
|
||||
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
animation={isRunning ? 'pulse' : undefined}
|
||||
iterationCount="infinite"
|
||||
easing="linear"
|
||||
duration={1000}
|
||||
useNativeDriver
|
||||
size={40}
|
||||
color={colors.text}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
this.updateStateColors();
|
||||
return (
|
||||
<Surface
|
||||
style={{
|
||||
...styles.container,
|
||||
height: props.height,
|
||||
borderRadius: 4,
|
||||
}}>
|
||||
{!isReady ? (
|
||||
<ProgressBar
|
||||
style={{
|
||||
...styles.progressBar,
|
||||
height: props.height,
|
||||
}}
|
||||
progress={progress}
|
||||
color={this.stateColors[machineState]}
|
||||
/>
|
||||
) : null}
|
||||
<List.Item
|
||||
title={this.title}
|
||||
description={description}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={this.onListItemPress}
|
||||
left={(): React.Node => icon}
|
||||
right={(): React.Node => (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<Text
|
||||
style={
|
||||
machineState === ProxiwashConstants.machineStates.FINISHED
|
||||
? {fontWeight: 'bold'}
|
||||
: {}
|
||||
}>
|
||||
{stateString}
|
||||
</Text>
|
||||
{machineState === ProxiwashConstants.machineStates.RUNNING ? (
|
||||
<Caption>{props.item.remainingTime} min</Caption>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<Avatar.Icon
|
||||
icon={stateIcon}
|
||||
color={colors.text}
|
||||
size={30}
|
||||
style={styles.icon}
|
||||
/>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 5,
|
||||
justifyContent: 'center',
|
||||
elevation: 1
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
progressBar: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default withTheme(ProxiwashListItem);
|
||||
|
|
|
|||
|
|
@ -1,72 +1,72 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Text, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
isDryer: boolean,
|
||||
nbAvailable: number,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
title: string,
|
||||
isDryer: boolean,
|
||||
nbAvailable: number,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
marginBottom: 10,
|
||||
marginTop: 20,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a proxiwash item, showing machine progression and state
|
||||
*/
|
||||
class ProxiwashListItem extends React.Component<Props> {
|
||||
class ProxiwashListItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.nbAvailable !== props.nbAvailable
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.nbAvailable !== this.props.nbAvailable)
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const subtitle = props.nbAvailable + ' ' + (
|
||||
(props.nbAvailable <= 1)
|
||||
? i18n.t('screens.proxiwash.numAvailable')
|
||||
: i18n.t('screens.proxiwash.numAvailablePlural'));
|
||||
const iconColor = props.nbAvailable > 0
|
||||
? this.props.theme.colors.success
|
||||
: this.props.theme.colors.primary;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Avatar.Icon
|
||||
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<Text style={styles.text}>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text style={{color: this.props.theme.colors.subtitle}}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const subtitle = `${props.nbAvailable} ${
|
||||
props.nbAvailable <= 1
|
||||
? i18n.t('screens.proxiwash.numAvailable')
|
||||
: i18n.t('screens.proxiwash.numAvailablePlural')
|
||||
}`;
|
||||
const iconColor =
|
||||
props.nbAvailable > 0
|
||||
? props.theme.colors.success
|
||||
: props.theme.colors.primary;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Avatar.Icon
|
||||
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<Text style={styles.text}>{props.title}</Text>
|
||||
<Text style={{color: props.theme.colors.subtitle}}>{subtitle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
marginBottom: 10,
|
||||
marginTop: 20,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
}
|
||||
});
|
||||
|
||||
export default withTheme(ProxiwashListItem);
|
||||
|
|
|
|||
|
|
@ -1,259 +1,269 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import {Image, TouchableWithoutFeedback, View} from "react-native";
|
||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Image, TouchableWithoutFeedback, View} from 'react-native';
|
||||
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
|
||||
type Props = {
|
||||
style?: ViewStyle,
|
||||
emotion: number,
|
||||
animated: boolean,
|
||||
entryAnimation: Animatable.AnimatableProperties | null,
|
||||
loopAnimation: Animatable.AnimatableProperties | null,
|
||||
onPress?: (viewRef: AnimatableViewRef) => null,
|
||||
onLongPress?: (viewRef: AnimatableViewRef) => null,
|
||||
}
|
||||
export type AnimatableViewRefType = {current: null | Animatable.View};
|
||||
|
||||
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 = {
|
||||
NORMAL: 0,
|
||||
HAPPY: 1,
|
||||
GIRLY: 2,
|
||||
WINK: 3,
|
||||
CUTE: 4,
|
||||
INTELLO: 5,
|
||||
LOVE: 6,
|
||||
COOL: 7,
|
||||
ANGRY: 8,
|
||||
RANDOM: 999,
|
||||
type PropsType = {
|
||||
emotion?: number,
|
||||
animated?: boolean,
|
||||
style?: ViewStyle | null,
|
||||
entryAnimation?: Animatable.AnimatableProperties | null,
|
||||
loopAnimation?: Animatable.AnimatableProperties | null,
|
||||
onPress?: null | ((viewRef: AnimatableViewRefType) => void),
|
||||
onLongPress?: null | ((viewRef: AnimatableViewRefType) => void),
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
currentEmotion: number,
|
||||
};
|
||||
|
||||
class Mascot extends React.Component<Props, State> {
|
||||
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');
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
export const EYE_STYLE = {
|
||||
NORMAL: 0,
|
||||
GIRLY: 2,
|
||||
CUTE: 3,
|
||||
WINK: 4,
|
||||
HEART: 5,
|
||||
ANGRY: 6,
|
||||
};
|
||||
|
||||
viewRef: AnimatableViewRef;
|
||||
eyeList: { [key: number]: number | string };
|
||||
glassesList: { [key: number]: number | string };
|
||||
const GLASSES_STYLE = {
|
||||
NORMAL: 0,
|
||||
COOl: 1,
|
||||
};
|
||||
|
||||
onPress: (viewRef: AnimatableViewRef) => null;
|
||||
onLongPress: (viewRef: AnimatableViewRef) => null;
|
||||
export const MASCOT_STYLE = {
|
||||
NORMAL: 0,
|
||||
HAPPY: 1,
|
||||
GIRLY: 2,
|
||||
WINK: 3,
|
||||
CUTE: 4,
|
||||
INTELLO: 5,
|
||||
LOVE: 6,
|
||||
COOL: 7,
|
||||
ANGRY: 8,
|
||||
RANDOM: 999,
|
||||
};
|
||||
|
||||
initialEmotion: number;
|
||||
class Mascot extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
emotion: MASCOT_STYLE.NORMAL,
|
||||
animated: false,
|
||||
style: null,
|
||||
entryAnimation: {
|
||||
useNativeDriver: true,
|
||||
animation: 'rubberBand',
|
||||
duration: 2000,
|
||||
},
|
||||
loopAnimation: {
|
||||
useNativeDriver: true,
|
||||
animation: 'swing',
|
||||
duration: 2000,
|
||||
iterationDelay: 250,
|
||||
iterationCount: 'infinite',
|
||||
},
|
||||
onPress: null,
|
||||
onLongPress: 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;
|
||||
viewRef: AnimatableViewRefType;
|
||||
|
||||
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES;
|
||||
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
|
||||
eyeList: {[key: number]: number | string};
|
||||
|
||||
this.initialEmotion = this.props.emotion;
|
||||
glassesList: {[key: number]: number | string};
|
||||
|
||||
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
|
||||
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
|
||||
onPress: (viewRef: AnimatableViewRefType) => void;
|
||||
|
||||
this.state = {
|
||||
currentEmotion: this.initialEmotion
|
||||
onLongPress: (viewRef: AnimatableViewRefType) => void;
|
||||
|
||||
initialEmotion: number;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
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;
|
||||
|
||||
this.initialEmotion =
|
||||
props.emotion != null ? props.emotion : Mascot.defaultProps.emotion;
|
||||
|
||||
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
|
||||
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
|
||||
|
||||
this.state = {
|
||||
currentEmotion: this.initialEmotion,
|
||||
};
|
||||
|
||||
if (props.onPress == null) {
|
||||
this.onPress = (viewRef: AnimatableViewRefType) => {
|
||||
const ref = viewRef.current;
|
||||
if (ref != null) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
|
||||
ref.rubberBand(1500).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
});
|
||||
}
|
||||
};
|
||||
} else this.onPress = props.onPress;
|
||||
|
||||
if (this.props.onPress == null) {
|
||||
this.onPress = (viewRef: AnimatableViewRef) => {
|
||||
let ref = viewRef.current;
|
||||
if (ref != null) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
|
||||
ref.rubberBand(1500).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
});
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else
|
||||
this.onPress = this.props.onPress;
|
||||
|
||||
if (this.props.onLongPress == null) {
|
||||
this.onLongPress = (viewRef: AnimatableViewRef) => {
|
||||
let ref = viewRef.current;
|
||||
if (ref != null) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
|
||||
ref.tada(1000).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
});
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else
|
||||
this.onLongPress = this.props.onLongPress;
|
||||
|
||||
}
|
||||
|
||||
getGlasses(style: number) {
|
||||
const glasses = this.glassesList[style];
|
||||
return <Image
|
||||
key={"glasses"}
|
||||
source={glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "15%",
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
getEye(style: number, isRight: boolean, rotation: string="0deg") {
|
||||
const eye = this.eyeList[style];
|
||||
return <Image
|
||||
key={isRight ? "right" : "left"}
|
||||
source={eye != null ? eye : this.eyeList[EYE_STYLE.NORMAL]}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "15%",
|
||||
left: isRight ? "-11%" : "11%",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transform: [{rotateY: rotation}]
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
getEyes(emotion: number) {
|
||||
let final = [];
|
||||
final.push(<View
|
||||
key={"container"}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}/>);
|
||||
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 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 (props.onLongPress == null) {
|
||||
this.onLongPress = (viewRef: AnimatableViewRefType) => {
|
||||
const ref = viewRef.current;
|
||||
if (ref != null) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
|
||||
ref.tada(1000).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
});
|
||||
}
|
||||
};
|
||||
} else this.onLongPress = props.onLongPress;
|
||||
}
|
||||
|
||||
if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses
|
||||
final.push(this.getGlasses(GLASSES_STYLE.NORMAL));
|
||||
getGlasses(style: number): React.Node {
|
||||
const glasses = this.glassesList[style];
|
||||
return (
|
||||
<Image
|
||||
key="glasses"
|
||||
source={
|
||||
glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]
|
||||
}
|
||||
final.push(<View key={"container2"}/>);
|
||||
return final;
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getEye(
|
||||
style: number,
|
||||
isRight: boolean,
|
||||
rotation: string = '0deg',
|
||||
): React.Node {
|
||||
const eye = this.eyeList[style];
|
||||
return (
|
||||
<Image
|
||||
key={isRight ? 'right' : 'left'}
|
||||
source={eye != null ? eye : this.eyeList[EYE_STYLE.NORMAL]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
left: isRight ? '-11%' : '11%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transform: [{rotateY: rotation}],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getEyes(emotion: number): React.Node {
|
||||
const final = [];
|
||||
final.push(
|
||||
<View
|
||||
key="container"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
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 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));
|
||||
}
|
||||
|
||||
render() {
|
||||
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
|
||||
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
|
||||
return (
|
||||
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;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
const entryAnimation = props.animated ? props.entryAnimation : null;
|
||||
const loopAnimation = props.animated ? props.loopAnimation : null;
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
...props.style,
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...entryAnimation}>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
this.onPress(this.viewRef);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
this.onLongPress(this.viewRef);
|
||||
}}>
|
||||
<Animatable.View ref={this.viewRef}>
|
||||
<Animatable.View
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...loopAnimation}>
|
||||
<Image
|
||||
source={MASCOT_IMAGE}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
...this.props.style
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
{...entryAnimation}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => this.onPress(this.viewRef)}
|
||||
onLongPress={() => this.onLongPress(this.viewRef)}
|
||||
>
|
||||
<Animatable.View
|
||||
ref={this.viewRef}
|
||||
>
|
||||
<Animatable.View
|
||||
{...loopAnimation}
|
||||
>
|
||||
<Image
|
||||
source={MASCOT_IMAGE}
|
||||
style={{
|
||||
width: "100%",
|
||||
height:"100%",
|
||||
}}
|
||||
/>
|
||||
{this.getEyes(this.state.currentEmotion)}
|
||||
</Animatable.View>
|
||||
</Animatable.View>
|
||||
</TouchableWithoutFeedback>
|
||||
/>
|
||||
{this.getEyes(state.currentEmotion)}
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
</Animatable.View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Mascot;
|
||||
|
|
|
|||
|
|
@ -1,283 +1,312 @@
|
|||
// @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 {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import SpeechArrow from "./SpeechArrow";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Paragraph,
|
||||
Portal,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Mascot from './Mascot';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import SpeechArrow from './SpeechArrow';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
|
||||
type Props = {
|
||||
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,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
icon: string,
|
||||
title: string,
|
||||
message: string,
|
||||
buttons: {
|
||||
action: {
|
||||
message: string,
|
||||
icon: string | null,
|
||||
color: string | null,
|
||||
onPress?: () => void,
|
||||
},
|
||||
emotion: number,
|
||||
visible?: boolean,
|
||||
prefKey?: string,
|
||||
}
|
||||
cancel: {
|
||||
message: string,
|
||||
icon: string | null,
|
||||
color: string | null,
|
||||
onPress?: () => void,
|
||||
},
|
||||
},
|
||||
emotion: number,
|
||||
visible?: boolean,
|
||||
prefKey?: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
|
||||
dialogVisible: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
|
||||
dialogVisible: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to display a popup with the mascot.
|
||||
*/
|
||||
class MascotPopup extends React.Component<Props, State> {
|
||||
class MascotPopup extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
visible: null,
|
||||
prefKey: null,
|
||||
};
|
||||
|
||||
mascotSize: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
mascotSize: number;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
windowWidth: number;
|
||||
|
||||
this.windowWidth = Dimensions.get('window').width;
|
||||
this.windowHeight = Dimensions.get('window').height;
|
||||
windowHeight: number;
|
||||
|
||||
this.mascotSize = Dimensions.get('window').height / 6;
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
if (this.props.visible != null) {
|
||||
this.state = {
|
||||
shouldRenderDialog: this.props.visible,
|
||||
dialogVisible: this.props.visible,
|
||||
};
|
||||
} else if (this.props.prefKey != null) {
|
||||
const visible = AsyncStorageManager.getBool(this.props.prefKey);
|
||||
this.state = {
|
||||
shouldRenderDialog: visible,
|
||||
dialogVisible: visible,
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
shouldRenderDialog: false,
|
||||
dialogVisible: false,
|
||||
};
|
||||
}
|
||||
this.windowWidth = Dimensions.get('window').width;
|
||||
this.windowHeight = Dimensions.get('window').height;
|
||||
|
||||
this.mascotSize = Dimensions.get('window').height / 6;
|
||||
|
||||
if (props.visible != null) {
|
||||
this.state = {
|
||||
shouldRenderDialog: props.visible,
|
||||
dialogVisible: props.visible,
|
||||
};
|
||||
} else if (props.prefKey != null) {
|
||||
const visible = AsyncStorageManager.getBool(props.prefKey);
|
||||
this.state = {
|
||||
shouldRenderDialog: visible,
|
||||
dialogVisible: visible,
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
shouldRenderDialog: false,
|
||||
dialogVisible: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onAnimationEnd = () => {
|
||||
this.setState({
|
||||
shouldRenderDialog: false,
|
||||
})
|
||||
componentDidMount(): * {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
|
||||
const {props, state} = this;
|
||||
if (nextProps.visible) {
|
||||
this.state.shouldRenderDialog = true;
|
||||
this.state.dialogVisible = true;
|
||||
} else if (
|
||||
nextProps.visible !== props.visible ||
|
||||
(!nextState.dialogVisible &&
|
||||
nextState.dialogVisible !== state.dialogVisible)
|
||||
) {
|
||||
this.state.dialogVisible = false;
|
||||
setTimeout(this.onAnimationEnd, 300);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
||||
if (nextProps.visible) {
|
||||
this.state.shouldRenderDialog = true;
|
||||
this.state.dialogVisible = true;
|
||||
} else if (nextProps.visible !== this.props.visible
|
||||
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
|
||||
this.state.dialogVisible = false;
|
||||
setTimeout(this.onAnimationEnd, 300);
|
||||
}
|
||||
return true;
|
||||
onAnimationEnd = () => {
|
||||
this.setState({
|
||||
shouldRenderDialog: false,
|
||||
});
|
||||
};
|
||||
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
const {state, props} = this;
|
||||
if (state.dialogVisible) {
|
||||
const {cancel} = props.buttons;
|
||||
const {action} = props.buttons;
|
||||
if (cancel != null) this.onDismiss(cancel.onPress);
|
||||
else this.onDismiss(action.onPress);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
componentDidMount(): * {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
)
|
||||
}
|
||||
|
||||
onBackButtonPressAndroid = () => {
|
||||
if (this.state.dialogVisible) {
|
||||
const cancel = this.props.buttons.cancel;
|
||||
const action = this.props.buttons.action;
|
||||
if (cancel != null)
|
||||
this.onDismiss(cancel.onPress);
|
||||
else
|
||||
this.onDismiss(action.onPress);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
getSpeechBubble() {
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
marginLeft: "10%",
|
||||
marginRight: "10%",
|
||||
}}
|
||||
useNativeDriver={true}
|
||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.state.dialogVisible ? 1000 : 300}
|
||||
>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.mascotSize / 3}}
|
||||
size={20}
|
||||
color={this.props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card style={{
|
||||
borderColor: this.props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={this.props.title}
|
||||
left={this.props.icon != null ?
|
||||
(props) => <Avatar.Icon
|
||||
{...props}
|
||||
size={48}
|
||||
style={{backgroundColor: "transparent"}}
|
||||
color={this.props.theme.colors.primary}
|
||||
icon={this.props.icon}
|
||||
/>
|
||||
|
||||
: null}
|
||||
getSpeechBubble(): React.Node {
|
||||
const {state, props} = this;
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
marginLeft: '10%',
|
||||
marginRight: '10%',
|
||||
}}
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={state.dialogVisible ? 1000 : 300}>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.mascotSize / 3}}
|
||||
size={20}
|
||||
color={props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
borderColor: props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={props.title}
|
||||
left={
|
||||
props.icon != null
|
||||
? (): React.Node => (
|
||||
<Avatar.Icon
|
||||
size={48}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
color={props.theme.colors.primary}
|
||||
icon={props.icon}
|
||||
/>
|
||||
|
||||
<Card.Content style={{
|
||||
maxHeight: this.windowHeight / 3
|
||||
}}>
|
||||
<ScrollView>
|
||||
<Paragraph style={{marginBottom: 10}}>
|
||||
{this.props.message}
|
||||
</Paragraph>
|
||||
</ScrollView>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Actions style={{marginTop: 10, marginBottom: 10}}>
|
||||
{this.getButtons()}
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
|
||||
getMascot() {
|
||||
return (
|
||||
<Animatable.View
|
||||
useNativeDriver={true}
|
||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
||||
duration={this.state.dialogVisible ? 1500 : 200}
|
||||
>
|
||||
<Mascot
|
||||
style={{width: this.mascotSize}}
|
||||
animated={true}
|
||||
emotion={this.props.emotion}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
|
||||
getButtons() {
|
||||
const action = this.props.buttons.action;
|
||||
const cancel = this.props.buttons.cancel;
|
||||
return (
|
||||
<View style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Card.Content
|
||||
style={{
|
||||
maxHeight: this.windowHeight / 3,
|
||||
}}>
|
||||
{action != null
|
||||
? <Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
mode={"contained"}
|
||||
icon={action.icon}
|
||||
color={action.color}
|
||||
onPress={() => this.onDismiss(action.onPress)}
|
||||
>
|
||||
{action.message}
|
||||
</Button>
|
||||
: null}
|
||||
{cancel != null
|
||||
? <Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
mode={"contained"}
|
||||
icon={cancel.icon}
|
||||
color={cancel.color}
|
||||
onPress={() => this.onDismiss(cancel.onPress)}
|
||||
>
|
||||
{cancel.message}
|
||||
</Button>
|
||||
: null}
|
||||
<ScrollView>
|
||||
<Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
|
||||
</ScrollView>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Actions style={{marginTop: 10, marginBottom: 10}}>
|
||||
{this.getButtons()}
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
|
||||
getMascot(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<Animatable.View
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={state.dialogVisible ? 1500 : 200}>
|
||||
<Mascot
|
||||
style={{width: this.mascotSize}}
|
||||
animated
|
||||
emotion={props.emotion}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
|
||||
getButtons(): React.Node {
|
||||
const {props} = this;
|
||||
const {action} = props.buttons;
|
||||
const {cancel} = props.buttons;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
{action != null ? (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
mode="contained"
|
||||
icon={action.icon}
|
||||
color={action.color}
|
||||
onPress={() => {
|
||||
this.onDismiss(action.onPress);
|
||||
}}>
|
||||
{action.message}
|
||||
</Button>
|
||||
) : null}
|
||||
{cancel != null ? (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
mode="contained"
|
||||
icon={cancel.icon}
|
||||
color={cancel.color}
|
||||
onPress={() => {
|
||||
this.onDismiss(cancel.onPress);
|
||||
}}>
|
||||
{cancel.message}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getBackground(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
this.onDismiss(props.buttons.cancel.onPress);
|
||||
}}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
|
||||
duration={state.dialogVisible ? 300 : 300}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
onDismiss = (callback?: () => void) => {
|
||||
const {prefKey} = this.props;
|
||||
if (prefKey != null) {
|
||||
AsyncStorageManager.set(prefKey, false);
|
||||
this.setState({dialogVisible: false});
|
||||
}
|
||||
if (callback != null) callback();
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {shouldRenderDialog} = this.state;
|
||||
if (shouldRenderDialog) {
|
||||
return (
|
||||
<Portal>
|
||||
{this.getBackground()}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
marginTop: -80,
|
||||
width: '100%',
|
||||
}}>
|
||||
{this.getMascot()}
|
||||
{this.getSpeechBubble()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getBackground() {
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={() => this.onDismiss(this.props.buttons.cancel.onPress)}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
useNativeDriver={true}
|
||||
animation={this.state.dialogVisible ? "fadeIn" : "fadeOut"}
|
||||
duration={this.state.dialogVisible ? 300 : 300}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
onDismiss = (callback?: ()=> void) => {
|
||||
if (this.props.prefKey != null) {
|
||||
AsyncStorageManager.set(this.props.prefKey, false);
|
||||
this.setState({dialogVisible: false});
|
||||
}
|
||||
if (callback != null)
|
||||
callback();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.shouldRenderDialog) {
|
||||
return (
|
||||
<Portal>
|
||||
{this.getBackground()}
|
||||
<View style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}>
|
||||
<View style={{
|
||||
marginTop: -80,
|
||||
width: "100%"
|
||||
}}>
|
||||
{this.getMascot()}
|
||||
{this.getSpeechBubble()}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</Portal>
|
||||
);
|
||||
} else
|
||||
return null;
|
||||
|
||||
</View>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(MascotPopup);
|
||||
|
|
|
|||
|
|
@ -1,33 +1,43 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import {View} from 'react-native';
|
||||
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
|
||||
type Props = {
|
||||
style?: ViewStyle,
|
||||
size: number,
|
||||
color: string,
|
||||
}
|
||||
|
||||
export default class SpeechArrow extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={this.props.style}>
|
||||
<View style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: this.props.size,
|
||||
borderBottomWidth: this.props.size,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: this.props.color,
|
||||
}}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
type PropsType = {
|
||||
style?: ViewStyle | null,
|
||||
size: number,
|
||||
color: string,
|
||||
};
|
||||
|
||||
export default class SpeechArrow extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
style: null,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<View
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: props.size,
|
||||
borderBottomWidth: props.size,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: props.color,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,63 @@
|
|||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {Agenda} from "react-native-calendars";
|
||||
// @flow
|
||||
|
||||
type Props = {
|
||||
theme: Object,
|
||||
}
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {Agenda} from 'react-native-calendars';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
onRef: (ref: Agenda) => void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class CustomAgenda extends React.Component<Props> {
|
||||
class CustomAgenda extends React.Component<PropsType> {
|
||||
getAgenda(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Agenda
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
ref={props.onRef}
|
||||
theme={{
|
||||
backgroundColor: props.theme.colors.agendaBackgroundColor,
|
||||
calendarBackground: props.theme.colors.background,
|
||||
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
|
||||
selectedDayBackgroundColor: props.theme.colors.primary,
|
||||
selectedDayTextColor: '#ffffff',
|
||||
todayTextColor: props.theme.colors.primary,
|
||||
dayTextColor: props.theme.colors.text,
|
||||
textDisabledColor: props.theme.colors.agendaDayTextColor,
|
||||
dotColor: props.theme.colors.primary,
|
||||
selectedDotColor: '#ffffff',
|
||||
arrowColor: 'orange',
|
||||
monthTextColor: props.theme.colors.primary,
|
||||
indicatorColor: props.theme.colors.primary,
|
||||
textDayFontWeight: '300',
|
||||
textMonthFontWeight: 'bold',
|
||||
textDayHeaderFontWeight: '300',
|
||||
textDayFontSize: 16,
|
||||
textMonthFontSize: 16,
|
||||
textDayHeaderFontSize: 16,
|
||||
agendaDayTextColor: props.theme.colors.agendaDayTextColor,
|
||||
agendaDayNumColor: props.theme.colors.agendaDayTextColor,
|
||||
agendaTodayColor: props.theme.colors.primary,
|
||||
agendaKnobColor: props.theme.colors.primary,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getAgenda() {
|
||||
return <Agenda
|
||||
{...this.props}
|
||||
ref={this.props.onRef}
|
||||
theme={{
|
||||
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
|
||||
calendarBackground: this.props.theme.colors.background,
|
||||
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
|
||||
selectedDayBackgroundColor: this.props.theme.colors.primary,
|
||||
selectedDayTextColor: '#ffffff',
|
||||
todayTextColor: this.props.theme.colors.primary,
|
||||
dayTextColor: this.props.theme.colors.text,
|
||||
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
|
||||
dotColor: this.props.theme.colors.primary,
|
||||
selectedDotColor: '#ffffff',
|
||||
arrowColor: 'orange',
|
||||
monthTextColor: this.props.theme.colors.primary,
|
||||
indicatorColor: this.props.theme.colors.primary,
|
||||
textDayFontWeight: '300',
|
||||
textMonthFontWeight: 'bold',
|
||||
textDayHeaderFontWeight: '300',
|
||||
textDayFontSize: 16,
|
||||
textMonthFontSize: 16,
|
||||
textDayHeaderFontSize: 16,
|
||||
agendaDayTextColor: this.props.theme.colors.agendaDayTextColor,
|
||||
agendaDayNumColor: this.props.theme.colors.agendaDayTextColor,
|
||||
agendaTodayColor: this.props.theme.colors.primary,
|
||||
agendaKnobColor: this.props.theme.colors.primary,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Completely recreate the component on theme change to force theme reload
|
||||
if (this.props.theme.dark)
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{this.getAgenda()}
|
||||
</View>
|
||||
);
|
||||
else
|
||||
return this.getAgenda();
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
// Completely recreate the component on theme change to force theme reload
|
||||
if (props.theme.dark)
|
||||
return <View style={{flex: 1}}>{this.getAgenda()}</View>;
|
||||
return this.getAgenda();
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(CustomAgenda);
|
||||
|
|
|
|||
|
|
@ -1,47 +1,58 @@
|
|||
/* eslint-disable flowtype/require-parameter-type */
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Text, withTheme} from 'react-native-paper';
|
||||
import HTML from "react-native-render-html";
|
||||
import {Linking} from "react-native";
|
||||
import HTML from 'react-native-render-html';
|
||||
import {Linking} from 'react-native';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
theme: Object,
|
||||
html: string,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
html: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class CustomHTML extends React.Component<Props> {
|
||||
class CustomHTML extends React.Component<PropsType> {
|
||||
openWebLink = (event: {...}, link: string) => {
|
||||
Linking.openURL(link);
|
||||
};
|
||||
|
||||
openWebLink = (event, link) => {
|
||||
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
|
||||
};
|
||||
getBasicText = (
|
||||
htmlAttribs,
|
||||
children,
|
||||
convertedCSSStyles,
|
||||
passProps,
|
||||
): React.Node => {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <Text {...passProps}>{children}</Text>;
|
||||
};
|
||||
|
||||
getBasicText = (htmlAttribs, children, convertedCSSStyles, passProps) => {
|
||||
return <Text {...passProps}>{children}</Text>;
|
||||
};
|
||||
getListBullet = (): React.Node => {
|
||||
return <Text>- </Text>;
|
||||
};
|
||||
|
||||
getListBullet = (htmlAttribs, children, convertedCSSStyles, passProps) => {
|
||||
return (
|
||||
<Text>- </Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
// Surround description with p to allow text styling if the description is not html
|
||||
return <HTML
|
||||
html={"<p>" + this.props.html + "</p>"}
|
||||
renderers={{
|
||||
p: this.getBasicText,
|
||||
li: this.getBasicText,
|
||||
}}
|
||||
listsPrefixesRenderers={{
|
||||
ul: this.getListBullet
|
||||
}}
|
||||
ignoredTags={['img']}
|
||||
ignoredStyles={['color', 'background-color']}
|
||||
onLinkPress={this.openWebLink}/>;
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
// Surround description with p to allow text styling if the description is not html
|
||||
return (
|
||||
<HTML
|
||||
html={`<p>${props.html}</p>`}
|
||||
renderers={{
|
||||
p: this.getBasicText,
|
||||
li: this.getBasicText,
|
||||
}}
|
||||
listsPrefixesRenderers={{
|
||||
ul: this.getListBullet,
|
||||
}}
|
||||
ignoredTags={['img']}
|
||||
ignoredStyles={['color', 'background-color']}
|
||||
onLinkPress={this.openWebLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(CustomHTML);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,39 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons';
|
||||
import {withTheme} from "react-native-paper";
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
const MaterialHeaderButton = (props: Object) =>
|
||||
const MaterialHeaderButton = (props: {
|
||||
theme: CustomThemeType,
|
||||
color: string,
|
||||
}): React.Node => {
|
||||
const {color, theme} = props;
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<HeaderButton
|
||||
{...props}
|
||||
IconComponent={MaterialCommunityIcons}
|
||||
iconSize={26}
|
||||
color={props.color != null ? props.color : props.theme.colors.text}
|
||||
/>;
|
||||
|
||||
const MaterialHeaderButtons = (props: Object) => {
|
||||
return (
|
||||
<HeaderButtons
|
||||
{...props}
|
||||
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
|
||||
/>
|
||||
);
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
IconComponent={MaterialCommunityIcons}
|
||||
iconSize={26}
|
||||
color={color != null ? color : theme.colors.text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTheme(MaterialHeaderButtons);
|
||||
const MaterialHeaderButtons = (props: {...}): React.Node => {
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<HeaderButtons
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialHeaderButtons;
|
||||
|
||||
export {Item} from 'react-navigation-header-buttons';
|
||||
|
|
|
|||
|
|
@ -1,411 +1,403 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
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 {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 i18n from 'i18n-js';
|
||||
import AppIntroSlider from "react-native-app-intro-slider";
|
||||
import Update from "../../constants/Update";
|
||||
import ThemeManager from "../../managers/ThemeManager";
|
||||
import AppIntroSlider from 'react-native-app-intro-slider';
|
||||
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";
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Card} from 'react-native-paper';
|
||||
import Update from '../../constants/Update';
|
||||
import ThemeManager from '../../managers/ThemeManager';
|
||||
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
|
||||
|
||||
type Props = {
|
||||
onDone: Function,
|
||||
isUpdate: boolean,
|
||||
isAprilFools: boolean,
|
||||
type PropsType = {
|
||||
onDone: () => void,
|
||||
isUpdate: boolean,
|
||||
isAprilFools: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
currentSlide: number,
|
||||
}
|
||||
|
||||
type Slide = {
|
||||
key: string,
|
||||
title: string,
|
||||
text: string,
|
||||
view: () => React.Node,
|
||||
mascotStyle: number,
|
||||
colors: [string, string]
|
||||
type StateType = {
|
||||
currentSlide: number,
|
||||
};
|
||||
|
||||
type IntroSlideType = {
|
||||
key: string,
|
||||
title: string,
|
||||
text: string,
|
||||
view: () => React.Node,
|
||||
mascotStyle: number,
|
||||
colors: [string, string],
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContent: {
|
||||
paddingBottom: 100,
|
||||
},
|
||||
text: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
color: 'white',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
center: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Class used to create intro slides
|
||||
*/
|
||||
export default class CustomIntroSlider extends React.Component<Props, State> {
|
||||
export default class CustomIntroSlider extends React.Component<
|
||||
PropsType,
|
||||
StateType,
|
||||
> {
|
||||
sliderRef: {current: null | AppIntroSlider};
|
||||
|
||||
state = {
|
||||
currentSlide: 0,
|
||||
}
|
||||
introSlides: Array<IntroSlideType>;
|
||||
|
||||
sliderRef: { current: null | AppIntroSlider };
|
||||
updateSlides: Array<IntroSlideType>;
|
||||
|
||||
introSlides: Array<Slide>;
|
||||
updateSlides: Array<Slide>;
|
||||
aprilFoolsSlides: Array<Slide>;
|
||||
currentSlides: Array<Slide>;
|
||||
aprilFoolsSlides: Array<IntroSlideType>;
|
||||
|
||||
/**
|
||||
* Generates intro slides
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.sliderRef = React.createRef();
|
||||
this.introSlides = [
|
||||
{
|
||||
key: '0', // Mascot
|
||||
title: i18n.t('intro.slideMain.title'),
|
||||
text: i18n.t('intro.slideMain.text'),
|
||||
view: this.getWelcomeView,
|
||||
mascotStyle: MASCOT_STYLE.NORMAL,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
title: i18n.t('intro.slidePlanex.title'),
|
||||
text: i18n.t('intro.slidePlanex.text'),
|
||||
view: () => this.getIconView("calendar-clock"),
|
||||
mascotStyle: MASCOT_STYLE.INTELLO,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
title: i18n.t('intro.slideEvents.title'),
|
||||
text: i18n.t('intro.slideEvents.text'),
|
||||
view: () => this.getIconView("calendar-star",),
|
||||
mascotStyle: MASCOT_STYLE.HAPPY,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
title: i18n.t('intro.slideServices.title'),
|
||||
text: i18n.t('intro.slideServices.text'),
|
||||
view: () => this.getIconView("view-dashboard-variant",),
|
||||
mascotStyle: MASCOT_STYLE.CUTE,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: i18n.t('intro.slideDone.title'),
|
||||
text: i18n.t('intro.slideDone.text'),
|
||||
view: () => this.getEndView(),
|
||||
mascotStyle: MASCOT_STYLE.COOL,
|
||||
colors: ['#9c165b', '#3e042b'],
|
||||
},
|
||||
];
|
||||
this.updateSlides = [];
|
||||
for (let i = 0; i < Update.slidesNumber; i++) {
|
||||
this.updateSlides.push(
|
||||
{
|
||||
key: i.toString(),
|
||||
title: Update.getInstance().titleList[i],
|
||||
text: Update.getInstance().descriptionList[i],
|
||||
icon: Update.iconList[i],
|
||||
colors: Update.colorsList[i],
|
||||
},
|
||||
);
|
||||
}
|
||||
currentSlides: Array<IntroSlideType>;
|
||||
|
||||
this.aprilFoolsSlides = [
|
||||
{
|
||||
key: '1',
|
||||
title: i18n.t('intro.aprilFoolsSlide.title'),
|
||||
text: i18n.t('intro.aprilFoolsSlide.text'),
|
||||
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
|
||||
*/
|
||||
getIntroRenderItem = ({item, dimensions}: { item: Slide, dimensions: { width: number, height: number } }) => {
|
||||
const index = parseInt(item.key);
|
||||
return (
|
||||
<LinearGradient
|
||||
style={[
|
||||
styles.mainContent,
|
||||
dimensions
|
||||
]}
|
||||
colors={item.colors}
|
||||
start={{x: 0, y: 0.1}}
|
||||
end={{x: 0.1, y: 1}}
|
||||
>
|
||||
{this.state.currentSlide === index
|
||||
? <View style={{height: "100%", flex: 1}}>
|
||||
<View style={{flex: 1}}>
|
||||
{item.view()}
|
||||
</View>
|
||||
<Animatable.View
|
||||
animation={"fadeIn"}>
|
||||
{index !== 0 && index !== this.introSlides.length - 1
|
||||
?
|
||||
<Mascot
|
||||
style={{
|
||||
marginLeft: 30,
|
||||
marginBottom: 0,
|
||||
width: 100,
|
||||
marginTop: -30,
|
||||
}}
|
||||
emotion={item.mascotStyle}
|
||||
animated={true}
|
||||
entryAnimation={{
|
||||
animation: "slideInLeft",
|
||||
duration: 500
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: "pulse",
|
||||
iterationCount: "infinite",
|
||||
duration: 2000,
|
||||
}}
|
||||
/> : 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>
|
||||
);
|
||||
}
|
||||
|
||||
getEndView = () => {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
height: "80%"
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated={true}
|
||||
entryAnimation={{
|
||||
animation: "slideInDown",
|
||||
duration: 2000,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: "pulse",
|
||||
duration: 2000,
|
||||
iterationCount: "infinite"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getWelcomeView = () => {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
height: "80%"
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
animated={true}
|
||||
entryAnimation={{
|
||||
animation: "bounceIn",
|
||||
duration: 2000,
|
||||
}}
|
||||
/>
|
||||
<Animatable.Text
|
||||
useNativeDriver={true}
|
||||
animation={"fadeInUp"}
|
||||
duration={500}
|
||||
|
||||
style={{
|
||||
color: "#fff",
|
||||
textAlign: "center",
|
||||
fontSize: 25,
|
||||
}}>
|
||||
PABLO
|
||||
</Animatable.Text>
|
||||
<Animatable.View
|
||||
useNativeDriver={true}
|
||||
animation={"fadeInUp"}
|
||||
duration={500}
|
||||
delay={200}
|
||||
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 30,
|
||||
right: "20%",
|
||||
width: 50,
|
||||
height: 50,
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
...styles.center,
|
||||
transform: [{rotateZ: "70deg"}],
|
||||
}}
|
||||
name={"undo"}
|
||||
color={'#fff'}
|
||||
size={40}/>
|
||||
</Animatable.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);
|
||||
}
|
||||
|
||||
onSlideChange = (index: number, lastIndex: number) => {
|
||||
this.setStatusBarColor(this.currentSlides[index].colors[0]);
|
||||
this.setState({currentSlide: index});
|
||||
/**
|
||||
* Generates intro slides
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
currentSlide: 0,
|
||||
};
|
||||
|
||||
onSkip = () => {
|
||||
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 = React.createRef();
|
||||
this.introSlides = [
|
||||
{
|
||||
key: '0', // Mascot
|
||||
title: i18n.t('intro.slideMain.title'),
|
||||
text: i18n.t('intro.slideMain.text'),
|
||||
view: this.getWelcomeView,
|
||||
mascotStyle: MASCOT_STYLE.NORMAL,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
title: i18n.t('intro.slidePlanex.title'),
|
||||
text: i18n.t('intro.slidePlanex.text'),
|
||||
view: (): React.Node => CustomIntroSlider.getIconView('calendar-clock'),
|
||||
mascotStyle: MASCOT_STYLE.INTELLO,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
title: i18n.t('intro.slideEvents.title'),
|
||||
text: i18n.t('intro.slideEvents.text'),
|
||||
view: (): React.Node => CustomIntroSlider.getIconView('calendar-star'),
|
||||
mascotStyle: MASCOT_STYLE.HAPPY,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
title: i18n.t('intro.slideServices.title'),
|
||||
text: i18n.t('intro.slideServices.text'),
|
||||
view: (): React.Node =>
|
||||
CustomIntroSlider.getIconView('view-dashboard-variant'),
|
||||
mascotStyle: MASCOT_STYLE.CUTE,
|
||||
colors: ['#be1522', '#57080e'],
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: i18n.t('intro.slideDone.title'),
|
||||
text: i18n.t('intro.slideDone.text'),
|
||||
view: (): React.Node => this.getEndView(),
|
||||
mascotStyle: MASCOT_STYLE.COOL,
|
||||
colors: ['#9c165b', '#3e042b'],
|
||||
},
|
||||
];
|
||||
// $FlowFixMe
|
||||
this.updateSlides = [];
|
||||
for (let i = 0; i < Update.slidesNumber; i += 1) {
|
||||
this.updateSlides.push({
|
||||
key: i.toString(),
|
||||
title: Update.getInstance().titleList[i],
|
||||
text: Update.getInstance().descriptionList[i],
|
||||
icon: Update.iconList[i],
|
||||
colors: Update.colorsList[i],
|
||||
});
|
||||
}
|
||||
|
||||
onDone = () => {
|
||||
this.setStatusBarColor(ThemeManager.getCurrentTheme().colors.surface);
|
||||
this.props.onDone();
|
||||
}
|
||||
|
||||
renderNextButton = () => {
|
||||
return (
|
||||
<Animatable.View
|
||||
animation={"fadeIn"}
|
||||
this.aprilFoolsSlides = [
|
||||
{
|
||||
key: '1',
|
||||
title: i18n.t('intro.aprilFoolsSlide.title'),
|
||||
text: i18n.t('intro.aprilFoolsSlide.text'),
|
||||
view: (): React.Node => <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
|
||||
*/
|
||||
getIntroRenderItem = ({
|
||||
item,
|
||||
dimensions,
|
||||
}: {
|
||||
item: IntroSlideType,
|
||||
dimensions: {width: number, height: number},
|
||||
}): React.Node => {
|
||||
const {state} = this;
|
||||
const index = parseInt(item.key, 10);
|
||||
return (
|
||||
<LinearGradient
|
||||
style={[styles.mainContent, dimensions]}
|
||||
colors={item.colors}
|
||||
start={{x: 0, y: 0.1}}
|
||||
end={{x: 0.1, y: 1}}>
|
||||
{state.currentSlide === index ? (
|
||||
<View style={{height: '100%', flex: 1}}>
|
||||
<View style={{flex: 1}}>{item.view()}</View>
|
||||
<Animatable.View animation="fadeIn">
|
||||
{index !== 0 && index !== this.introSlides.length - 1 ? (
|
||||
<Mascot
|
||||
style={{
|
||||
marginLeft: 30,
|
||||
marginBottom: 0,
|
||||
width: 100,
|
||||
marginTop: -30,
|
||||
}}
|
||||
emotion={item.mascotStyle}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'slideInLeft',
|
||||
duration: 500,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: 'pulse',
|
||||
iterationCount: 'infinite',
|
||||
duration: 2000,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
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"}
|
||||
|
||||
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={{
|
||||
borderRadius: 25,
|
||||
padding: 5,
|
||||
backgroundColor: "rgb(190,21,34)"
|
||||
backgroundColor: 'rgba(0,0,0,0.38)',
|
||||
marginHorizontal: 20,
|
||||
borderColor: 'rgba(0,0,0,0.60)',
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
name={"check"}
|
||||
color={'#fff'}
|
||||
size={40}/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
this.currentSlides = this.introSlides;
|
||||
if (this.props.isUpdate)
|
||||
this.currentSlides = this.updateSlides;
|
||||
else if (this.props.isAprilFools)
|
||||
this.currentSlides = this.aprilFoolsSlides;
|
||||
this.setStatusBarColor(this.currentSlides[0].colors[0]);
|
||||
return (
|
||||
<AppIntroSlider
|
||||
ref={this.sliderRef}
|
||||
data={this.currentSlides}
|
||||
extraData={this.state.currentSlide}
|
||||
getEndView = (): React.Node => {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
height: '80%',
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'slideInDown',
|
||||
duration: 2000,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: 'pulse',
|
||||
duration: 2000,
|
||||
iterationCount: 'infinite',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem={this.getIntroRenderItem}
|
||||
renderNextButton={this.renderNextButton}
|
||||
renderDoneButton={this.renderDoneButton}
|
||||
getWelcomeView = (): React.Node => {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
height: '80%',
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 2000,
|
||||
}}
|
||||
/>
|
||||
<Animatable.Text
|
||||
useNativeDriver
|
||||
animation="fadeInUp"
|
||||
duration={500}
|
||||
style={{
|
||||
color: '#fff',
|
||||
textAlign: 'center',
|
||||
fontSize: 25,
|
||||
}}>
|
||||
PABLO
|
||||
</Animatable.Text>
|
||||
<Animatable.View
|
||||
useNativeDriver
|
||||
animation="fadeInUp"
|
||||
duration={500}
|
||||
delay={200}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 30,
|
||||
right: '20%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
...styles.center,
|
||||
transform: [{rotateZ: '70deg'}],
|
||||
}}
|
||||
name="undo"
|
||||
color="#fff"
|
||||
size={40}
|
||||
/>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
onDone={this.onDone}
|
||||
onSlideChange={this.onSlideChange}
|
||||
onSkip={this.onSkip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
static getIconView(icon: MaterialCommunityIconsGlyphs): React.Node {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Animatable.View style={styles.center} animation="fadeIn">
|
||||
<MaterialCommunityIcons name={icon} color="#fff" size={200} />
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
static setStatusBarColor(color: string) {
|
||||
if (Platform.OS === 'android') StatusBar.setBackgroundColor(color, true);
|
||||
}
|
||||
|
||||
onSlideChange = (index: number) => {
|
||||
CustomIntroSlider.setStatusBarColor(this.currentSlides[index].colors[0]);
|
||||
this.setState({currentSlide: index});
|
||||
};
|
||||
|
||||
onSkip = () => {
|
||||
CustomIntroSlider.setStatusBarColor(
|
||||
this.currentSlides[this.currentSlides.length - 1].colors[0],
|
||||
);
|
||||
if (this.sliderRef.current != null)
|
||||
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
|
||||
};
|
||||
|
||||
onDone = () => {
|
||||
const {props} = this;
|
||||
CustomIntroSlider.setStatusBarColor(
|
||||
ThemeManager.getCurrentTheme().colors.surface,
|
||||
);
|
||||
props.onDone();
|
||||
};
|
||||
|
||||
getRenderNextButton = (): React.Node => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
getRenderDoneButton = (): React.Node => {
|
||||
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(): React.Node {
|
||||
const {props, state} = this;
|
||||
this.currentSlides = this.introSlides;
|
||||
if (props.isUpdate) this.currentSlides = this.updateSlides;
|
||||
else if (props.isAprilFools) this.currentSlides = this.aprilFoolsSlides;
|
||||
CustomIntroSlider.setStatusBarColor(this.currentSlides[0].colors[0]);
|
||||
return (
|
||||
<AppIntroSlider
|
||||
ref={this.sliderRef}
|
||||
data={this.currentSlides}
|
||||
extraData={state.currentSlide}
|
||||
renderItem={this.getIntroRenderItem}
|
||||
renderNextButton={this.getRenderNextButton}
|
||||
renderDoneButton={this.getRenderDoneButton}
|
||||
onDone={this.onDone}
|
||||
onSlideChange={this.onSlideChange}
|
||||
onSkip={this.onSkip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContent: {
|
||||
paddingBottom: 100,
|
||||
},
|
||||
text: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
color: 'white',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
center: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {Modalize} from "react-native-modalize";
|
||||
import {View} from "react-native-animatable";
|
||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
||||
import {Modalize} from 'react-native-modalize';
|
||||
import {View} from 'react-native-animatable';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
/**
|
||||
* Abstraction layer for Modalize component, using custom configuration
|
||||
|
|
@ -12,25 +13,29 @@ import CustomTabBar from "../Tabbar/CustomTabBar";
|
|||
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
||||
* @return {*}
|
||||
*/
|
||||
function CustomModal(props) {
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<Modalize
|
||||
ref={props.onRef}
|
||||
adjustToContentHeight
|
||||
handlePosition={'inside'}
|
||||
modalStyle={{backgroundColor: colors.card}}
|
||||
handleStyle={{backgroundColor: colors.primary}}
|
||||
>
|
||||
<View style={{
|
||||
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT
|
||||
}}>
|
||||
{props.children}
|
||||
</View>
|
||||
|
||||
</Modalize>
|
||||
);
|
||||
function CustomModal(props: {
|
||||
theme: CustomThemeType,
|
||||
onRef: (re: Modalize) => void,
|
||||
children?: React.Node,
|
||||
}): React.Node {
|
||||
const {theme, onRef, children} = props;
|
||||
return (
|
||||
<Modalize
|
||||
ref={onRef}
|
||||
adjustToContentHeight
|
||||
handlePosition="inside"
|
||||
modalStyle={{backgroundColor: theme.colors.card}}
|
||||
handleStyle={{backgroundColor: theme.colors.primary}}>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
{children}
|
||||
</View>
|
||||
</Modalize>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(CustomModal);
|
||||
CustomModal.defaultProps = {children: null};
|
||||
|
||||
export default withTheme(CustomModal);
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Text, withTheme} from 'react-native-paper';
|
||||
import {View} from "react-native-animatable";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import Slider, {SliderProps} from "@react-native-community/slider";
|
||||
import {View} from 'react-native-animatable';
|
||||
import Slider, {SliderProps} from '@react-native-community/slider';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
theme: CustomTheme,
|
||||
valueSuffix: string,
|
||||
...SliderProps
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
valueSuffix?: string,
|
||||
...SliderProps,
|
||||
};
|
||||
|
||||
type State = {
|
||||
currentValue: number,
|
||||
}
|
||||
type StateType = {
|
||||
currentValue: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer for Modalize component, using custom configuration
|
||||
|
|
@ -22,37 +22,44 @@ type State = {
|
|||
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
||||
* @return {*}
|
||||
*/
|
||||
class CustomSlider extends React.Component<Props, State> {
|
||||
class CustomSlider extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
valueSuffix: '',
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
valueSuffix: "",
|
||||
}
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentValue: props.value,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
currentValue: this.props.value,
|
||||
}
|
||||
|
||||
onValueChange = (value: number) => {
|
||||
this.setState({currentValue: value});
|
||||
if (this.props.onValueChange != null)
|
||||
this.props.onValueChange(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<Text style={{marginHorizontal: 10, marginTop: 'auto', marginBottom: 'auto'}}>
|
||||
{this.state.currentValue}min
|
||||
</Text>
|
||||
<Slider
|
||||
{...this.props}
|
||||
onValueChange={this.onValueChange}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
onValueChange = (value: number) => {
|
||||
const {props} = this;
|
||||
this.setState({currentValue: value});
|
||||
if (props.onValueChange != null) props.onValueChange(value);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<Text
|
||||
style={{
|
||||
marginHorizontal: 10,
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
{state.currentValue}min
|
||||
</Text>
|
||||
<Slider
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
onValueChange={this.onValueChange}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(CustomSlider);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {ActivityIndicator, withTheme} from 'react-native-paper';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
/**
|
||||
* Component used to display a header button
|
||||
|
|
@ -10,28 +11,29 @@ import {ActivityIndicator, withTheme} from 'react-native-paper';
|
|||
* @param props Props to pass to the component
|
||||
* @return {*}
|
||||
*/
|
||||
function BasicLoadingScreen(props) {
|
||||
const {colors} = props.theme;
|
||||
let position = undefined;
|
||||
if (props.isAbsolute !== undefined && props.isAbsolute)
|
||||
position = 'absolute';
|
||||
function BasicLoadingScreen(props: {
|
||||
theme: CustomThemeType,
|
||||
isAbsolute: boolean,
|
||||
}): React.Node {
|
||||
const {theme, isAbsolute} = props;
|
||||
const {colors} = theme;
|
||||
let position;
|
||||
if (isAbsolute != null && isAbsolute) position = 'absolute';
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
backgroundColor: colors.background,
|
||||
position: position,
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size={'large'}
|
||||
color={colors.primary}/>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
position,
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator animating size="large" color={colors.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(BasicLoadingScreen);
|
||||
|
|
|
|||
|
|
@ -2,191 +2,192 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Button, Subheading, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import i18n from 'i18n-js';
|
||||
import {ERROR_TYPE} from "../../utils/WebData";
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
navigation: Object,
|
||||
route: Object,
|
||||
errorCode: number,
|
||||
onRefresh: Function,
|
||||
icon: string,
|
||||
message: string,
|
||||
showRetryButton: boolean,
|
||||
}
|
||||
|
||||
type State = {
|
||||
refreshing: boolean,
|
||||
}
|
||||
|
||||
class ErrorView extends React.PureComponent<Props, State> {
|
||||
|
||||
colors: Object;
|
||||
|
||||
message: string;
|
||||
icon: string;
|
||||
|
||||
showLoginButton: boolean;
|
||||
|
||||
static defaultProps = {
|
||||
errorCode: 0,
|
||||
icon: '',
|
||||
message: '',
|
||||
showRetryButton: true,
|
||||
}
|
||||
|
||||
state = {
|
||||
refreshing: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.colors = props.theme.colors;
|
||||
this.icon = "";
|
||||
}
|
||||
|
||||
generateMessage() {
|
||||
this.showLoginButton = false;
|
||||
if (this.props.errorCode !== 0) {
|
||||
switch (this.props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t("errors.badCredentials");
|
||||
this.icon = "account-alert-outline";
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t("errors.badToken");
|
||||
this.icon = "account-alert-outline";
|
||||
this.showLoginButton = true;
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t("errors.noConsent");
|
||||
this.icon = "account-remove-outline";
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_SAVE:
|
||||
this.message = i18n.t("errors.tokenSave");
|
||||
this.icon = "alert-circle-outline";
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t("errors.badInput");
|
||||
this.icon = "alert-circle-outline";
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t("errors.forbidden");
|
||||
this.icon = "lock";
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t("errors.connectionError");
|
||||
this.icon = "access-point-network-off";
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t("errors.serverError");
|
||||
this.icon = "server-network-off";
|
||||
break;
|
||||
default:
|
||||
this.message = i18n.t("errors.unknown");
|
||||
this.icon = "alert-circle-outline";
|
||||
break;
|
||||
}
|
||||
this.message += "\n\nCode " + this.props.errorCode;
|
||||
} else {
|
||||
this.message = this.props.message;
|
||||
this.icon = this.props.icon;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getRetryButton() {
|
||||
return <Button
|
||||
mode={'contained'}
|
||||
icon={'refresh'}
|
||||
onPress={this.props.onRefresh}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("general.retry")}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
goToLogin = () => {
|
||||
this.props.navigation.navigate("login",
|
||||
{
|
||||
screen: 'login',
|
||||
params: {nextScreen: this.props.route.name}
|
||||
})
|
||||
};
|
||||
|
||||
getLoginButton() {
|
||||
return <Button
|
||||
mode={'contained'}
|
||||
icon={'login'}
|
||||
onPress={this.goToLogin}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("screens.login.title")}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.generateMessage();
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
...styles.outer,
|
||||
backgroundColor: this.colors.background
|
||||
}}
|
||||
animation={"zoomIn"}
|
||||
duration={200}
|
||||
useNativeDriver
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name={this.icon}
|
||||
size={150}
|
||||
color={this.colors.textDisabled}/>
|
||||
</View>
|
||||
<Subheading style={{
|
||||
...styles.subheading,
|
||||
color: this.colors.textDisabled
|
||||
}}>
|
||||
{this.message}
|
||||
</Subheading>
|
||||
{this.props.showRetryButton
|
||||
? (this.showLoginButton
|
||||
? this.getLoginButton()
|
||||
: this.getRetryButton())
|
||||
: null}
|
||||
</View>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
route: {name: string},
|
||||
onRefresh?: () => void,
|
||||
errorCode?: number,
|
||||
icon?: string,
|
||||
message?: string,
|
||||
showRetryButton?: boolean,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
height: '100%',
|
||||
},
|
||||
inner: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
iconContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 20
|
||||
},
|
||||
subheading: {
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
button: {
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}
|
||||
outer: {
|
||||
height: '100%',
|
||||
},
|
||||
inner: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
iconContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 20,
|
||||
},
|
||||
subheading: {
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
button: {
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
class ErrorView extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
onRefresh: () => {},
|
||||
errorCode: 0,
|
||||
icon: '',
|
||||
message: '',
|
||||
showRetryButton: true,
|
||||
};
|
||||
|
||||
message: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
showLoginButton: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.icon = '';
|
||||
}
|
||||
|
||||
getRetryButton(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="refresh"
|
||||
onPress={props.onRefresh}
|
||||
style={styles.button}>
|
||||
{i18n.t('general.retry')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
getLoginButton(): React.Node {
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="login"
|
||||
onPress={this.goToLogin}
|
||||
style={styles.button}>
|
||||
{i18n.t('screens.login.title')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
goToLogin = () => {
|
||||
const {props} = this;
|
||||
props.navigation.navigate('login', {
|
||||
screen: 'login',
|
||||
params: {nextScreen: props.route.name},
|
||||
});
|
||||
};
|
||||
|
||||
generateMessage() {
|
||||
const {props} = this;
|
||||
this.showLoginButton = false;
|
||||
if (props.errorCode !== 0) {
|
||||
switch (props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t('errors.badCredentials');
|
||||
this.icon = 'account-alert-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t('errors.badToken');
|
||||
this.icon = 'account-alert-outline';
|
||||
this.showLoginButton = true;
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t('errors.noConsent');
|
||||
this.icon = 'account-remove-outline';
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_SAVE:
|
||||
this.message = i18n.t('errors.tokenSave');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t('errors.badInput');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t('errors.forbidden');
|
||||
this.icon = 'lock';
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t('errors.connectionError');
|
||||
this.icon = 'access-point-network-off';
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t('errors.serverError');
|
||||
this.icon = 'server-network-off';
|
||||
break;
|
||||
default:
|
||||
this.message = i18n.t('errors.unknown');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
}
|
||||
this.message += `\n\nCode ${
|
||||
props.errorCode != null ? props.errorCode : -1
|
||||
}`;
|
||||
} else {
|
||||
this.message = props.message != null ? props.message : '';
|
||||
this.icon = props.icon != null ? props.icon : '';
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
this.generateMessage();
|
||||
let button;
|
||||
if (this.showLoginButton) button = this.getLoginButton();
|
||||
else if (props.showRetryButton) button = this.getRetryButton();
|
||||
else button = null;
|
||||
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
...styles.outer,
|
||||
backgroundColor: props.theme.colors.background,
|
||||
}}
|
||||
animation="zoomIn"
|
||||
duration={200}
|
||||
useNativeDriver>
|
||||
<View style={styles.inner}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
// $FlowFixMe
|
||||
name={this.icon}
|
||||
size={150}
|
||||
color={props.theme.colors.textDisabled}
|
||||
/>
|
||||
</View>
|
||||
<Subheading
|
||||
style={{
|
||||
...styles.subheading,
|
||||
color: props.theme.colors.textDisabled,
|
||||
}}>
|
||||
{this.message}
|
||||
</Subheading>
|
||||
{button}
|
||||
</View>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ErrorView);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {Collapsible} from 'react-navigation-collapsible';
|
|||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import {withCollapsible} from '../../utils/withCollapsible';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import {ERROR_TYPE, readData} from '../../utils/WebData';
|
||||
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
|
||||
|
|
|
|||
|
|
@ -1,233 +1,247 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import WebView from "react-native-webview";
|
||||
import BasicLoadingScreen from "./BasicLoadingScreen";
|
||||
import ErrorView from "./ErrorView";
|
||||
import {ERROR_TYPE} from "../../utils/WebData";
|
||||
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
||||
import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons";
|
||||
import WebView from 'react-native-webview';
|
||||
import {
|
||||
Divider,
|
||||
HiddenItem,
|
||||
OverflowMenu,
|
||||
} from 'react-navigation-header-buttons';
|
||||
import i18n from 'i18n-js';
|
||||
import {Animated, BackHandler, Linking} from "react-native";
|
||||
import {withCollapsible} from "../../utils/withCollapsible";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import {withTheme} from "react-native-paper";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {Collapsible} from "react-navigation-collapsible";
|
||||
import {Animated, BackHandler, Linking} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
url: string,
|
||||
customJS: string,
|
||||
customPaddingFunction: null | (padding: number) => string,
|
||||
collapsibleStack: Collapsible,
|
||||
onMessage: Function,
|
||||
onScroll: Function,
|
||||
showAdvancedControls: boolean,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
url: string,
|
||||
collapsibleStack: Collapsible,
|
||||
onMessage: (event: {nativeEvent: {data: string}}) => void,
|
||||
onScroll: (event: SyntheticEvent<EventTarget>) => void,
|
||||
customJS?: string,
|
||||
customPaddingFunction?: null | ((padding: number) => string),
|
||||
showAdvancedControls?: boolean,
|
||||
};
|
||||
|
||||
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
|
||||
|
||||
/**
|
||||
* Class defining a webview screen.
|
||||
*/
|
||||
class WebViewScreen extends React.PureComponent<Props> {
|
||||
class WebViewScreen extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
customJS: '',
|
||||
showAdvancedControls: true,
|
||||
customPaddingFunction: null,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
customJS: '',
|
||||
showAdvancedControls: true,
|
||||
customPaddingFunction: null,
|
||||
};
|
||||
webviewRef: {current: null | WebView};
|
||||
|
||||
webviewRef: { current: null | WebView };
|
||||
canGoBack: boolean;
|
||||
|
||||
canGoBack: boolean;
|
||||
constructor() {
|
||||
super();
|
||||
this.webviewRef = React.createRef();
|
||||
this.canGoBack = false;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.webviewRef = React.createRef();
|
||||
this.canGoBack = false;
|
||||
/**
|
||||
* Creates header buttons and listens to events after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
const {props} = this;
|
||||
props.navigation.setOptions({
|
||||
headerRight: props.showAdvancedControls
|
||||
? this.getAdvancedButtons
|
||||
: this.getBasicButton,
|
||||
});
|
||||
props.navigation.addListener('focus', () => {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
});
|
||||
props.navigation.addListener('blur', () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back on the webview or on the navigation stack if we cannot go back anymore
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
if (this.canGoBack) {
|
||||
this.onGoBackClicked();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates header buttons and listens to events after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerRight: this.props.showAdvancedControls
|
||||
? this.getAdvancedButtons
|
||||
: this.getBasicButton,
|
||||
});
|
||||
this.props.navigation.addListener(
|
||||
'focus',
|
||||
() =>
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
)
|
||||
);
|
||||
this.props.navigation.addListener(
|
||||
'blur',
|
||||
() =>
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Gets header refresh and open in browser buttons
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getBasicButton = (): React.Node => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
/>
|
||||
<Item
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
iconName="open-in-new"
|
||||
onPress={this.onOpenClicked}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Goes back on the webview or on the navigation stack if we cannot go back anymore
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = () => {
|
||||
if (this.canGoBack) {
|
||||
this.onGoBackClicked();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets header refresh and open in browser buttons
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getBasicButton = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}/>
|
||||
<Item
|
||||
title={i18n.t("general.openInBrowser")}
|
||||
iconName="open-in-new"
|
||||
onPress={this.onOpenClicked}/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates advanced header control buttons.
|
||||
* These buttons allows the user to refresh, go back, go forward and open in the browser.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAdvancedButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
/>
|
||||
<OverflowMenu
|
||||
style={{marginHorizontal: 10}}
|
||||
OverflowIcon={
|
||||
<MaterialCommunityIcons
|
||||
name="dots-vertical"
|
||||
size={26}
|
||||
color={this.props.theme.colors.text}
|
||||
/>}
|
||||
>
|
||||
<HiddenItem
|
||||
title={i18n.t("general.goBack")}
|
||||
onPress={this.onGoBackClicked}/>
|
||||
<HiddenItem
|
||||
title={i18n.t("general.goForward")}
|
||||
onPress={this.onGoForwardClicked}/>
|
||||
<Divider/>
|
||||
<HiddenItem
|
||||
title={i18n.t("general.openInBrowser")}
|
||||
onPress={this.onOpenClicked}/>
|
||||
</OverflowMenu>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to use when refresh button is clicked. Reloads the webview.
|
||||
*/
|
||||
onRefreshClicked = () => {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.reload();
|
||||
}
|
||||
onGoBackClicked = () => {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.goBack();
|
||||
}
|
||||
onGoForwardClicked = () => {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.goForward();
|
||||
}
|
||||
onOpenClicked = () => Linking.openURL(this.props.url);
|
||||
|
||||
/**
|
||||
* Injects the given javascript string into the web page
|
||||
*
|
||||
* @param script The script to inject
|
||||
*/
|
||||
injectJavaScript = (script: string) => {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.injectJavaScript(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the loading indicator
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderLoading = () => <BasicLoadingScreen isAbsolute={true}/>;
|
||||
|
||||
/**
|
||||
* Gets the javascript needed to generate a padding on top of the page
|
||||
* This adds padding to the body and runs the custom padding function given in props
|
||||
*
|
||||
* @param padding The padding to add in pixels
|
||||
* @returns {string}
|
||||
*/
|
||||
getJavascriptPadding(padding: number) {
|
||||
const customPadding = this.props.customPaddingFunction != null ? this.props.customPaddingFunction(padding) : "";
|
||||
return (
|
||||
"document.getElementsByTagName('body')[0].style.paddingTop = '" + padding + "px';" +
|
||||
customPadding +
|
||||
"true;"
|
||||
);
|
||||
}
|
||||
|
||||
onScroll = (event: Object) => {
|
||||
if (this.props.onScroll)
|
||||
this.props.onScroll(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={this.webviewRef}
|
||||
source={{uri: this.props.url}}
|
||||
startInLoadingState={true}
|
||||
injectedJavaScript={this.props.customJS}
|
||||
javaScriptEnabled={true}
|
||||
renderLoading={this.getRenderLoading}
|
||||
renderError={() => <ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefreshClicked}
|
||||
/>}
|
||||
onNavigationStateChange={navState => {
|
||||
this.canGoBack = navState.canGoBack;
|
||||
}}
|
||||
onMessage={this.props.onMessage}
|
||||
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
|
||||
// Animations
|
||||
onScroll={onScrollWithListener(this.onScroll)}
|
||||
/**
|
||||
* Creates advanced header control buttons.
|
||||
* These buttons allows the user to refresh, go back, go forward and open in the browser.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAdvancedButtons = (): React.Node => {
|
||||
const {props} = this;
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
/>
|
||||
<OverflowMenu
|
||||
style={{marginHorizontal: 10}}
|
||||
OverflowIcon={
|
||||
<MaterialCommunityIcons
|
||||
name="dots-vertical"
|
||||
size={26}
|
||||
color={props.theme.colors.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goBack')}
|
||||
onPress={this.onGoBackClicked}
|
||||
/>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goForward')}
|
||||
onPress={this.onGoForwardClicked}
|
||||
/>
|
||||
<Divider />
|
||||
<HiddenItem
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
onPress={this.onOpenClicked}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the loading indicator
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />;
|
||||
|
||||
/**
|
||||
* Gets the javascript needed to generate a padding on top of the page
|
||||
* This adds padding to the body and runs the custom padding function given in props
|
||||
*
|
||||
* @param padding The padding to add in pixels
|
||||
* @returns {string}
|
||||
*/
|
||||
getJavascriptPadding(padding: number): string {
|
||||
const {props} = this;
|
||||
const customPadding =
|
||||
props.customPaddingFunction != null
|
||||
? props.customPaddingFunction(padding)
|
||||
: '';
|
||||
return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to use when refresh button is clicked. Reloads the webview.
|
||||
*/
|
||||
onRefreshClicked = () => {
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.reload();
|
||||
};
|
||||
|
||||
onGoBackClicked = () => {
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
|
||||
};
|
||||
|
||||
onGoForwardClicked = () => {
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
|
||||
};
|
||||
|
||||
onOpenClicked = () => {
|
||||
const {url} = this.props;
|
||||
Linking.openURL(url);
|
||||
};
|
||||
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
const {onScroll} = this.props;
|
||||
if (onScroll) onScroll(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the given javascript string into the web page
|
||||
*
|
||||
* @param script The script to inject
|
||||
*/
|
||||
injectJavaScript = (script: string) => {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.injectJavaScript(script);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={this.webviewRef}
|
||||
source={{uri: props.url}}
|
||||
startInLoadingState
|
||||
injectedJavaScript={props.customJS}
|
||||
javaScriptEnabled
|
||||
renderLoading={this.getRenderLoading}
|
||||
renderError={(): React.Node => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefreshClicked}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
|
||||
this.canGoBack = navState.canGoBack;
|
||||
}}
|
||||
onMessage={props.onMessage}
|
||||
onLoad={() => {
|
||||
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
|
||||
}}
|
||||
// Animations
|
||||
onScroll={onScrollWithListener(this.onScroll)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCollapsible(withTheme(WebViewScreen));
|
||||
|
|
|
|||
|
|
@ -2,179 +2,216 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import TabIcon from "./TabIcon";
|
||||
import TabHomeIcon from "./TabHomeIcon";
|
||||
import {Animated} from 'react-native';
|
||||
import {Collapsible} from "react-navigation-collapsible";
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import TabIcon from './TabIcon';
|
||||
import TabHomeIcon from './TabHomeIcon';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
state: Object,
|
||||
descriptors: Object,
|
||||
navigation: Object,
|
||||
theme: Object,
|
||||
collapsibleStack: Object,
|
||||
}
|
||||
|
||||
type State = {
|
||||
translateY: AnimatedValue,
|
||||
barSynced: boolean,
|
||||
}
|
||||
|
||||
const TAB_ICONS = {
|
||||
proxiwash: 'tshirt-crew',
|
||||
services: 'account-circle',
|
||||
planning: 'calendar-range',
|
||||
planex: 'clock',
|
||||
type RouteType = {
|
||||
name: string,
|
||||
key: string,
|
||||
params: {collapsible: Collapsible},
|
||||
state: {
|
||||
index: number,
|
||||
routes: Array<RouteType>,
|
||||
},
|
||||
};
|
||||
|
||||
class CustomTabBar extends React.Component<Props, State> {
|
||||
type PropsType = {
|
||||
state: {
|
||||
index: number,
|
||||
routes: Array<RouteType>,
|
||||
},
|
||||
descriptors: {
|
||||
[key: string]: {
|
||||
options: {
|
||||
tabBarLabel: string,
|
||||
title: string,
|
||||
},
|
||||
},
|
||||
},
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
static TAB_BAR_HEIGHT = 48;
|
||||
type StateType = {
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
translateY: any,
|
||||
};
|
||||
|
||||
state = {
|
||||
translateY: new Animated.Value(0),
|
||||
}
|
||||
const TAB_ICONS = {
|
||||
proxiwash: 'tshirt-crew',
|
||||
services: 'account-circle',
|
||||
planning: 'calendar-range',
|
||||
planex: 'clock',
|
||||
};
|
||||
|
||||
syncTabBar = (route, index) => {
|
||||
const state = this.props.state;
|
||||
const isFocused = state.index === index;
|
||||
if (isFocused) {
|
||||
const stackState = route.state;
|
||||
const stackRoute = stackState ? stackState.routes[stackState.index] : undefined;
|
||||
const params: { collapsible: Collapsible } = stackRoute ? stackRoute.params : undefined;
|
||||
const collapsible = params ? params.collapsible : undefined;
|
||||
if (collapsible) {
|
||||
this.setState({
|
||||
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
|
||||
});
|
||||
}
|
||||
}
|
||||
class CustomTabBar extends React.Component<PropsType, StateType> {
|
||||
static TAB_BAR_HEIGHT = 48;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
translateY: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the given route if it is different from the current one
|
||||
*
|
||||
* @param route Destination route
|
||||
* @param currentIndex The current route index
|
||||
* @param destIndex The destination route index
|
||||
*/
|
||||
onItemPress(route: Object, currentIndex: number, destIndex: number) {
|
||||
const event = this.props.navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
/**
|
||||
* Navigates to the given route if it is different from the current one
|
||||
*
|
||||
* @param route Destination route
|
||||
* @param currentIndex The current route index
|
||||
* @param destIndex The destination route index
|
||||
*/
|
||||
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
|
||||
const {navigation} = this.props;
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (currentIndex !== destIndex && !event.defaultPrevented)
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to tetris screen on home button long press
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
onItemLongPress(route: RouteType) {
|
||||
const {navigation} = this.props;
|
||||
const event = navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (route.name === 'home' && !event.defaultPrevented)
|
||||
navigation.navigate('game-start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the active route and syncs the tab bar animation with the header bar
|
||||
*/
|
||||
onRouteChange = () => {
|
||||
const {props} = this;
|
||||
props.state.routes.map(this.syncTabBar);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an icon for the given route if it is not the home one as it uses a custom button
|
||||
*
|
||||
* @param route
|
||||
* @param focused
|
||||
* @returns {null}
|
||||
*/
|
||||
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => {
|
||||
let icon = TAB_ICONS[route.name];
|
||||
icon = focused ? icon : `${icon}-outline`;
|
||||
if (route.name !== 'home') return icon;
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a tab icon render.
|
||||
* If the given route is focused, it syncs the tab bar and header bar animations together
|
||||
*
|
||||
* @param route The route for the icon
|
||||
* @param index The index of the current route
|
||||
* @returns {*}
|
||||
*/
|
||||
getRenderIcon = (route: RouteType, index: number): React.Node => {
|
||||
const {props} = this;
|
||||
const {state} = props;
|
||||
const {options} = props.descriptors[route.key];
|
||||
let label;
|
||||
if (options.tabBarLabel != null) label = options.tabBarLabel;
|
||||
else if (options.title != null) label = options.title;
|
||||
else label = route.name;
|
||||
|
||||
const onPress = () => {
|
||||
this.onItemPress(route, state.index, index);
|
||||
};
|
||||
const onLongPress = () => {
|
||||
this.onItemLongPress(route);
|
||||
};
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const color = isFocused
|
||||
? props.theme.colors.primary
|
||||
: props.theme.colors.tabIcon;
|
||||
if (route.name !== 'home') {
|
||||
return (
|
||||
<TabIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
icon={this.getTabBarIcon(route, isFocused)}
|
||||
color={color}
|
||||
label={label}
|
||||
focused={isFocused}
|
||||
extraData={state.index > index}
|
||||
key={route.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TabHomeIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
focused={isFocused}
|
||||
key={route.key}
|
||||
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getIcons(): React.Node {
|
||||
const {props} = this;
|
||||
return props.state.routes.map(this.getRenderIcon);
|
||||
}
|
||||
|
||||
syncTabBar = (route: RouteType, index: number) => {
|
||||
const {state} = this.props;
|
||||
const isFocused = state.index === index;
|
||||
if (isFocused) {
|
||||
const stackState = route.state;
|
||||
const stackRoute =
|
||||
stackState != null ? stackState.routes[stackState.index] : null;
|
||||
const params: {collapsible: Collapsible} | null =
|
||||
stackRoute != null ? stackRoute.params : null;
|
||||
const collapsible = params != null ? params.collapsible : null;
|
||||
if (collapsible != null) {
|
||||
this.setState({
|
||||
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
|
||||
});
|
||||
if (currentIndex !== destIndex && !event.defaultPrevented)
|
||||
this.props.navigation.navigate(route.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to tetris screen on home button long press
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
onItemLongPress(route: Object) {
|
||||
const event = this.props.navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (route.name === "home" && !event.defaultPrevented)
|
||||
this.props.navigation.navigate('game-start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an icon for the given route if it is not the home one as it uses a custom button
|
||||
*
|
||||
* @param route
|
||||
* @param focused
|
||||
* @returns {null}
|
||||
*/
|
||||
tabBarIcon = (route, focused) => {
|
||||
let icon = TAB_ICONS[route.name];
|
||||
icon = focused ? icon : icon + ('-outline');
|
||||
if (route.name !== "home")
|
||||
return icon;
|
||||
else
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the active route and syncs the tab bar animation with the header bar
|
||||
*/
|
||||
onRouteChange = () => {
|
||||
this.props.state.routes.map(this.syncTabBar)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a tab icon render.
|
||||
* If the given route is focused, it syncs the tab bar and header bar animations together
|
||||
*
|
||||
* @param route The route for the icon
|
||||
* @param index The index of the current route
|
||||
* @returns {*}
|
||||
*/
|
||||
renderIcon = (route, index) => {
|
||||
const state = this.props.state;
|
||||
const {options} = this.props.descriptors[route.key];
|
||||
const label =
|
||||
options.tabBarLabel != null
|
||||
? options.tabBarLabel
|
||||
: options.title != null
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const onPress = () => this.onItemPress(route, state.index, index);
|
||||
const onLongPress = () => this.onItemLongPress(route);
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
|
||||
if (route.name !== "home") {
|
||||
return <TabIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
icon={this.tabBarIcon(route, isFocused)}
|
||||
color={color}
|
||||
label={label}
|
||||
focused={isFocused}
|
||||
extraData={state.index > index}
|
||||
key={route.key}
|
||||
/>
|
||||
} else
|
||||
return <TabHomeIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
focused={isFocused}
|
||||
key={route.key}
|
||||
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
|
||||
/>
|
||||
};
|
||||
|
||||
getIcons() {
|
||||
return this.props.state.routes.map(this.renderIcon);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.props.navigation.addListener('state', this.onRouteChange);
|
||||
const icons = this.getIcons();
|
||||
return (
|
||||
<Animated.View
|
||||
useNativeDriver
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
height: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: this.props.theme.colors.surface,
|
||||
transform: [{translateY: this.state.translateY}],
|
||||
}}
|
||||
>
|
||||
{icons}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
props.navigation.addListener('state', this.onRouteChange);
|
||||
const icons = this.getIcons();
|
||||
// $FlowFixMe
|
||||
return (
|
||||
<Animated.View
|
||||
useNativeDriver
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
height: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: props.theme.colors.surface,
|
||||
transform: [{translateY: state.translateY}],
|
||||
}}>
|
||||
{icons}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(CustomTabBar);
|
||||
|
|
|
|||
|
|
@ -1,106 +1,133 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Image, Platform, View} from "react-native";
|
||||
import {Image, Platform, View} from 'react-native';
|
||||
import {FAB, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import FOCUSED_ICON from '../../../assets/tab-icon.png';
|
||||
import UNFOCUSED_ICON from '../../../assets/tab-icon-outline.png';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
focused: boolean,
|
||||
onPress: Function,
|
||||
onLongPress: Function,
|
||||
theme: Object,
|
||||
tabBarHeight: number,
|
||||
}
|
||||
type PropsType = {
|
||||
focused: boolean,
|
||||
onPress: () => void,
|
||||
onLongPress: () => void,
|
||||
theme: CustomThemeType,
|
||||
tabBarHeight: number,
|
||||
};
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabHomeIcon extends React.Component<Props> {
|
||||
class TabHomeIcon extends React.Component<PropsType> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
'0': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.2,
|
||||
translateY: -9,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.1,
|
||||
translateY: -7,
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
'0': {
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
focusedIcon = require('../../../assets/tab-icon.png');
|
||||
unFocusedIcon = require('../../../assets/tab-icon-outline.png');
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {focused} = this.props;
|
||||
return nextProps.focused !== focused;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
"0": {
|
||||
scale: 1, translateY: 0
|
||||
},
|
||||
"0.9": {
|
||||
scale: 1.2, translateY: -9
|
||||
},
|
||||
"1": {
|
||||
scale: 1.1, translateY: -7
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
"0": {
|
||||
scale: 1.1, translateY: -6
|
||||
},
|
||||
"1": {
|
||||
scale: 1, translateY: 0
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
iconRender = ({size, color}) =>
|
||||
this.props.focused
|
||||
? <Image
|
||||
source={this.focusedIcon}
|
||||
style={{width: size, height: size, tintColor: color}}
|
||||
/>
|
||||
: <Image
|
||||
source={this.unFocusedIcon}
|
||||
style={{width: size, height: size, tintColor: color}}
|
||||
/>;
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
return (nextProps.focused !== this.props.focused);
|
||||
}
|
||||
|
||||
render(): React$Node {
|
||||
const props = this.props;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
borderless={true}
|
||||
rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: this.props.tabBarHeight + 30,
|
||||
marginBottom: -15,
|
||||
}}
|
||||
>
|
||||
<AnimatedFAB
|
||||
duration={200}
|
||||
easing={"ease-out"}
|
||||
animation={props.focused ? "fabFocusIn" : "fabFocusOut"}
|
||||
icon={this.iconRender}
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto'
|
||||
}}/>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
|
||||
);
|
||||
}
|
||||
getIconRender = ({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => {
|
||||
const {focused} = this.props;
|
||||
if (focused)
|
||||
return (
|
||||
<Image
|
||||
source={FOCUSED_ICON}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
tintColor: color,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Image
|
||||
source={UNFOCUSED_ICON}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
tintColor: color,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
borderless
|
||||
rippleColor={
|
||||
Platform.OS === 'android'
|
||||
? props.theme.colors.primary
|
||||
: 'transparent'
|
||||
}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: props.tabBarHeight + 30,
|
||||
marginBottom: -15,
|
||||
}}>
|
||||
<AnimatedFAB
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
|
||||
icon={this.getIconRender}
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(TabHomeIcon);
|
||||
export default withTheme(TabHomeIcon);
|
||||
|
|
|
|||
|
|
@ -1,114 +1,117 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import {View} from 'react-native';
|
||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
|
||||
type Props = {
|
||||
focused: boolean,
|
||||
color: string,
|
||||
label: string,
|
||||
icon: MaterialCommunityIconsGlyphs,
|
||||
onPress: Function,
|
||||
onLongPress: Function,
|
||||
theme: Object,
|
||||
extraData: any,
|
||||
}
|
||||
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
focused: boolean,
|
||||
color: string,
|
||||
label: string,
|
||||
icon: MaterialCommunityIconsGlyphs,
|
||||
onPress: () => void,
|
||||
onLongPress: () => void,
|
||||
theme: CustomThemeType,
|
||||
extraData: null | boolean | number | string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabIcon extends React.Component<Props> {
|
||||
class TabIcon extends React.Component<PropsType> {
|
||||
firstRender: boolean;
|
||||
|
||||
firstRender: boolean;
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
focusIn: {
|
||||
'0': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.3,
|
||||
translateY: 7,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
},
|
||||
focusOut: {
|
||||
'0': {
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.firstRender = true;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
focusIn: {
|
||||
"0": {
|
||||
scale: 1, translateY: 0
|
||||
},
|
||||
"0.9": {
|
||||
scale: 1.3, translateY: 7
|
||||
},
|
||||
"1": {
|
||||
scale: 1.2, translateY: 6
|
||||
},
|
||||
},
|
||||
focusOut: {
|
||||
"0": {
|
||||
scale: 1.2, translateY: 6
|
||||
},
|
||||
"1": {
|
||||
scale: 1, translateY: 0
|
||||
},
|
||||
}
|
||||
});
|
||||
this.firstRender = true;
|
||||
}
|
||||
componentDidMount() {
|
||||
this.firstRender = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.firstRender = false;
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.focused !== props.focused ||
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.extraData !== props.extraData
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
return (nextProps.focused !== this.props.focused)
|
||||
|| (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.extraData !== this.props.extraData);
|
||||
}
|
||||
|
||||
render(): React$Node {
|
||||
const props = this.props;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
borderless={true}
|
||||
rippleColor={this.props.theme.colors.primary}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Animatable.View
|
||||
duration={200}
|
||||
easing={"ease-out"}
|
||||
animation={props.focused ? "focusIn" : "focusOut"}
|
||||
useNativeDriver
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={props.icon}
|
||||
color={props.color}
|
||||
size={26}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Animatable.Text
|
||||
animation={props.focused ? "fadeOutDown" : "fadeIn"}
|
||||
useNativeDriver
|
||||
|
||||
style={{
|
||||
color: props.color,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
fontSize: 10,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</Animatable.Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
borderless
|
||||
rippleColor={props.theme.colors.primary}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<View>
|
||||
<Animatable.View
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'focusIn' : 'focusOut'}
|
||||
useNativeDriver>
|
||||
<MaterialCommunityIcons
|
||||
name={props.icon}
|
||||
color={props.color}
|
||||
size={26}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Animatable.Text
|
||||
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
|
||||
useNativeDriver
|
||||
style={{
|
||||
color: props.color,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
fontSize: 10,
|
||||
}}>
|
||||
{props.label}
|
||||
</Animatable.Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(TabIcon);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
export default {
|
||||
websites: {
|
||||
AMICALE: "https://www.amicale-insat.fr/",
|
||||
AVAILABLE_ROOMS: "http://planex.insa-toulouse.fr/salles.php",
|
||||
BIB: "https://bibbox.insa-toulouse.fr/",
|
||||
BLUEMIND: "https://etud-mel.insa-toulouse.fr/webmail/",
|
||||
ELUS_ETUDIANTS: "https://etud.insa-toulouse.fr/~eeinsat/",
|
||||
ENT: "https://ent.insa-toulouse.fr/",
|
||||
INSA_ACCOUNT: "https://moncompte.insa-toulouse.fr/",
|
||||
TUTOR_INSA: "https://www.etud.insa-toulouse.fr/~tutorinsa/",
|
||||
WIKETUD: "https://wiki.etud.insa-toulouse.fr/",
|
||||
},
|
||||
}
|
||||
websites: {
|
||||
AMICALE: 'https://www.amicale-insat.fr/',
|
||||
AVAILABLE_ROOMS: 'http://planex.insa-toulouse.fr/salles.php',
|
||||
BIB: 'https://bibbox.insa-toulouse.fr/',
|
||||
BLUEMIND: 'https://etud-mel.insa-toulouse.fr/webmail/',
|
||||
ELUS_ETUDIANTS: 'https://etud.insa-toulouse.fr/~eeinsat/',
|
||||
ENT: 'https://ent.insa-toulouse.fr/',
|
||||
INSA_ACCOUNT: 'https://moncompte.insa-toulouse.fr/',
|
||||
TUTOR_INSA: 'https://www.etud.insa-toulouse.fr/~tutorinsa/',
|
||||
WIKETUD: 'https://wiki.etud.insa-toulouse.fr/',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
export default {
|
||||
machineStates: {
|
||||
"AVAILABLE": 0,
|
||||
"RUNNING": 1,
|
||||
"RUNNING_NOT_STARTED": 2,
|
||||
"FINISHED": 3,
|
||||
"UNAVAILABLE": 4,
|
||||
"ERROR": 5,
|
||||
"UNKNOWN": 6,
|
||||
},
|
||||
stateIcons: {
|
||||
0: 'radiobox-blank',
|
||||
1: 'progress-check',
|
||||
2: 'alert-circle-outline',
|
||||
3: 'check-circle',
|
||||
4: 'alert-octagram-outline',
|
||||
5: 'alert',
|
||||
6: 'help-circle-outline',
|
||||
}
|
||||
machineStates: {
|
||||
AVAILABLE: 0,
|
||||
RUNNING: 1,
|
||||
RUNNING_NOT_STARTED: 2,
|
||||
FINISHED: 3,
|
||||
UNAVAILABLE: 4,
|
||||
ERROR: 5,
|
||||
UNKNOWN: 6,
|
||||
},
|
||||
stateIcons: {
|
||||
0: 'radiobox-blank',
|
||||
1: 'progress-check',
|
||||
2: 'alert-circle-outline',
|
||||
3: 'check-circle',
|
||||
4: 'alert-octagram-outline',
|
||||
5: 'alert',
|
||||
6: 'help-circle-outline',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import i18n from "i18n-js";
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
/**
|
||||
* Singleton used to manage update slides.
|
||||
|
|
@ -14,51 +14,47 @@ import i18n from "i18n-js";
|
|||
* </ul>
|
||||
*/
|
||||
export default class Update {
|
||||
// Increment the number to show the update slide
|
||||
static number = 6;
|
||||
|
||||
// Increment the number to show the update slide
|
||||
static number = 6;
|
||||
// Change the number of slides to display
|
||||
static slidesNumber = 4;
|
||||
// Change the icons to be displayed on the update slide
|
||||
static iconList = [
|
||||
'star',
|
||||
'clock',
|
||||
'qrcode-scan',
|
||||
'account',
|
||||
];
|
||||
static colorsList = [
|
||||
['#e01928', '#be1522'],
|
||||
['#7c33ec', '#5e11d1'],
|
||||
['#337aec', '#114ed1'],
|
||||
['#e01928', '#be1522'],
|
||||
]
|
||||
// Change the number of slides to display
|
||||
static slidesNumber = 4;
|
||||
|
||||
static instance: Update | null = null;
|
||||
// Change the icons to be displayed on the update slide
|
||||
static iconList = ['star', 'clock', 'qrcode-scan', 'account'];
|
||||
|
||||
titleList: Array<string>;
|
||||
descriptionList: Array<string>;
|
||||
static colorsList = [
|
||||
['#e01928', '#be1522'],
|
||||
['#7c33ec', '#5e11d1'],
|
||||
['#337aec', '#114ed1'],
|
||||
['#e01928', '#be1522'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Init translations
|
||||
*/
|
||||
constructor() {
|
||||
this.titleList = [];
|
||||
this.descriptionList = [];
|
||||
for (let i = 0; i < Update.slidesNumber; i++) {
|
||||
this.titleList.push(i18n.t('intro.updateSlide' + i + '.title'))
|
||||
this.descriptionList.push(i18n.t('intro.updateSlide' + i + '.text'))
|
||||
}
|
||||
static instance: Update | null = null;
|
||||
|
||||
titleList: Array<string>;
|
||||
|
||||
descriptionList: Array<string>;
|
||||
|
||||
/**
|
||||
* Init translations
|
||||
*/
|
||||
constructor() {
|
||||
this.titleList = [];
|
||||
this.descriptionList = [];
|
||||
for (let i = 0; i < Update.slidesNumber; i += 1) {
|
||||
this.titleList.push(i18n.t(`intro.updateSlide${i}.title`));
|
||||
this.descriptionList.push(i18n.t(`intro.updateSlide${i}.text`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
*
|
||||
* @returns {Update}
|
||||
*/
|
||||
static getInstance(): Update {
|
||||
return Update.instance === null ?
|
||||
Update.instance = new Update() :
|
||||
Update.instance;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
*
|
||||
* @returns {Update}
|
||||
*/
|
||||
static getInstance(): Update {
|
||||
if (Update.instance == null) Update.instance = new Update();
|
||||
return Update.instance;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,138 @@
|
|||
// @flow
|
||||
|
||||
import type {Machine} from "../screens/Proxiwash/ProxiwashScreen";
|
||||
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen';
|
||||
import type {CustomThemeType} from './ThemeManager';
|
||||
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
|
||||
|
||||
/**
|
||||
* Singleton class used to manage april fools
|
||||
*/
|
||||
export default class AprilFoolsManager {
|
||||
static instance: AprilFoolsManager | null = null;
|
||||
|
||||
static instance: AprilFoolsManager | null = null;
|
||||
static fakeMachineNumber = [
|
||||
"",
|
||||
"cos(ln(1))",
|
||||
"0,5⁻¹",
|
||||
"567/189",
|
||||
"√2×√8",
|
||||
"√50×sin(9π/4)",
|
||||
"⌈π+e⌉",
|
||||
"div(rot(B))+7",
|
||||
"4×cosh(0)+4",
|
||||
"8-(-i)²",
|
||||
"|5√2+5√2i|",
|
||||
"1×10¹+1×10⁰",
|
||||
"Re(√192e^(iπ/6))",
|
||||
];
|
||||
aprilFoolsEnabled: boolean;
|
||||
static fakeMachineNumber = [
|
||||
'',
|
||||
'cos(ln(1))',
|
||||
'0,5⁻¹',
|
||||
'567/189',
|
||||
'√2×√8',
|
||||
'√50×sin(9π/4)',
|
||||
'⌈π+e⌉',
|
||||
'div(rot(B))+7',
|
||||
'4×cosh(0)+4',
|
||||
'8-(-i)²',
|
||||
'|5√2+5√2i|',
|
||||
'1×10¹+1×10⁰',
|
||||
'Re(√192e^(iπ/6))',
|
||||
];
|
||||
|
||||
constructor() {
|
||||
let today = new Date();
|
||||
this.aprilFoolsEnabled = (today.getDate() === 1 && today.getMonth() === 3);
|
||||
aprilFoolsEnabled: boolean;
|
||||
|
||||
constructor() {
|
||||
const today = new Date();
|
||||
this.aprilFoolsEnabled = today.getDate() === 1 && today.getMonth() === 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {ThemeManager}
|
||||
*/
|
||||
static getInstance(): AprilFoolsManager {
|
||||
if (AprilFoolsManager.instance == null)
|
||||
AprilFoolsManager.instance = new AprilFoolsManager();
|
||||
return AprilFoolsManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fake menu entries
|
||||
*
|
||||
* @param menu
|
||||
* @returns {Object}
|
||||
*/
|
||||
static getFakeMenuItem(
|
||||
menu: Array<RuFoodCategoryType>,
|
||||
): Array<RuFoodCategoryType> {
|
||||
menu[1].dishes.splice(4, 0, {name: 'Coq au vin'});
|
||||
menu[1].dishes.splice(2, 0, {name: "Bat'Soupe"});
|
||||
menu[1].dishes.splice(1, 0, {name: 'Pave de loup'});
|
||||
menu[1].dishes.splice(0, 0, {name: 'Béranger à point'});
|
||||
menu[1].dishes.splice(0, 0, {name: "Pieds d'Arnaud"});
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes proxiwash dryers order
|
||||
*
|
||||
* @param dryers
|
||||
*/
|
||||
static getNewProxiwashDryerOrderedList(
|
||||
dryers: Array<ProxiwashMachineType> | null,
|
||||
) {
|
||||
if (dryers != null) {
|
||||
const second = dryers[1];
|
||||
dryers.splice(1, 1);
|
||||
dryers.push(second);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {ThemeManager}
|
||||
*/
|
||||
static getInstance(): AprilFoolsManager {
|
||||
return AprilFoolsManager.instance === null ?
|
||||
AprilFoolsManager.instance = new AprilFoolsManager() :
|
||||
AprilFoolsManager.instance;
|
||||
/**
|
||||
* Changes proxiwash washers order
|
||||
*
|
||||
* @param washers
|
||||
*/
|
||||
static getNewProxiwashWasherOrderedList(
|
||||
washers: Array<ProxiwashMachineType> | null,
|
||||
) {
|
||||
if (washers != null) {
|
||||
const first = washers[0];
|
||||
const second = washers[1];
|
||||
const fifth = washers[4];
|
||||
const ninth = washers[8];
|
||||
washers.splice(8, 1, second);
|
||||
washers.splice(4, 1, ninth);
|
||||
washers.splice(1, 1, first);
|
||||
washers.splice(0, 1, fifth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fake menu entries
|
||||
*
|
||||
* @param menu
|
||||
* @returns {Object}
|
||||
*/
|
||||
static getFakeMenuItem(menu: Array<{dishes: Array<{name: string}>}>) {
|
||||
menu[1]["dishes"].splice(4, 0, {name: "Coq au vin"});
|
||||
menu[1]["dishes"].splice(2, 0, {name: "Bat'Soupe"});
|
||||
menu[1]["dishes"].splice(1, 0, {name: "Pave de loup"});
|
||||
menu[1]["dishes"].splice(0, 0, {name: "Béranger à point"});
|
||||
menu[1]["dishes"].splice(0, 0, {name: "Pieds d'Arnaud"});
|
||||
return menu;
|
||||
}
|
||||
/**
|
||||
* Gets the new display number for the given machine number
|
||||
*
|
||||
* @param number
|
||||
* @returns {string}
|
||||
*/
|
||||
static getProxiwashMachineDisplayNumber(number: number): string {
|
||||
return AprilFoolsManager.fakeMachineNumber[number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes proxiwash dryers order
|
||||
*
|
||||
* @param dryers
|
||||
*/
|
||||
static getNewProxiwashDryerOrderedList(dryers: Array<Machine> | null) {
|
||||
if (dryers != null) {
|
||||
let second = dryers[1];
|
||||
dryers.splice(1, 1);
|
||||
dryers.push(second);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets the new and ugly april fools theme
|
||||
*
|
||||
* @param currentTheme
|
||||
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
|
||||
*/
|
||||
static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType {
|
||||
return {
|
||||
...currentTheme,
|
||||
colors: {
|
||||
...currentTheme.colors,
|
||||
primary: '#00be45',
|
||||
accent: '#00be45',
|
||||
background: '#d02eee',
|
||||
tabIcon: '#380d43',
|
||||
card: '#eed639',
|
||||
surface: '#eed639',
|
||||
dividerBackground: '#c72ce4',
|
||||
textDisabled: '#b9b9b9',
|
||||
|
||||
/**
|
||||
* Changes proxiwash washers order
|
||||
*
|
||||
* @param washers
|
||||
*/
|
||||
static getNewProxiwashWasherOrderedList(washers: Array<Machine> | null) {
|
||||
if (washers != null) {
|
||||
let first = washers[0];
|
||||
let second = washers[1];
|
||||
let fifth = washers[4];
|
||||
let ninth = washers[8];
|
||||
washers.splice(8, 1, second);
|
||||
washers.splice(4, 1, ninth);
|
||||
washers.splice(1, 1, first);
|
||||
washers.splice(0, 1, fifth);
|
||||
}
|
||||
}
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#c72ce4',
|
||||
agendaDayTextColor: '#6d6d6d',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new display number for the given machine number
|
||||
*
|
||||
* @param number
|
||||
* @returns {string}
|
||||
*/
|
||||
static getProxiwashMachineDisplayNumber(number: number) {
|
||||
return AprilFoolsManager.fakeMachineNumber[number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new and ugly april fools theme
|
||||
*
|
||||
* @param currentTheme
|
||||
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
|
||||
*/
|
||||
static getAprilFoolsTheme(currentTheme: Object) {
|
||||
return {
|
||||
...currentTheme,
|
||||
colors: {
|
||||
...currentTheme.colors,
|
||||
primary: '#00be45',
|
||||
accent: '#00be45',
|
||||
background: '#d02eee',
|
||||
tabIcon: "#380d43",
|
||||
card: "#eed639",
|
||||
surface: "#eed639",
|
||||
dividerBackground: '#c72ce4',
|
||||
textDisabled: '#b9b9b9',
|
||||
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#c72ce4',
|
||||
agendaDayTextColor: '#6d6d6d',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
isAprilFoolsEnabled() {
|
||||
return this.aprilFoolsEnabled;
|
||||
}
|
||||
|
||||
};
|
||||
isAprilFoolsEnabled(): boolean {
|
||||
return this.aprilFoolsEnabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {SERVICES_KEY} from "./ServicesManager";
|
||||
import {SERVICES_KEY} from './ServicesManager';
|
||||
|
||||
/**
|
||||
* Singleton used to manage preferences.
|
||||
|
|
@ -10,227 +10,232 @@ import {SERVICES_KEY} from "./ServicesManager";
|
|||
*/
|
||||
|
||||
export default class AsyncStorageManager {
|
||||
static instance: AsyncStorageManager | null = null;
|
||||
|
||||
static instance: AsyncStorageManager | null = null;
|
||||
static PREFERENCES = {
|
||||
debugUnlocked: {
|
||||
key: 'debugUnlocked',
|
||||
default: '0',
|
||||
},
|
||||
showIntro: {
|
||||
key: 'showIntro',
|
||||
default: '1',
|
||||
},
|
||||
updateNumber: {
|
||||
key: 'updateNumber',
|
||||
default: '0',
|
||||
},
|
||||
proxiwashNotifications: {
|
||||
key: 'proxiwashNotifications',
|
||||
default: '5',
|
||||
},
|
||||
nightModeFollowSystem: {
|
||||
key: 'nightModeFollowSystem',
|
||||
default: '1',
|
||||
},
|
||||
nightMode: {
|
||||
key: 'nightMode',
|
||||
default: '1',
|
||||
},
|
||||
defaultStartScreen: {
|
||||
key: 'defaultStartScreen',
|
||||
default: 'home',
|
||||
},
|
||||
servicesShowBanner: {
|
||||
key: 'servicesShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
proxiwashShowBanner: {
|
||||
key: 'proxiwashShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
homeShowBanner: {
|
||||
key: 'homeShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
eventsShowBanner: {
|
||||
key: 'eventsShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
planexShowBanner: {
|
||||
key: 'planexShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
loginShowBanner: {
|
||||
key: 'loginShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
voteShowBanner: {
|
||||
key: 'voteShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
equipmentShowBanner: {
|
||||
key: 'equipmentShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
gameStartShowBanner: {
|
||||
key: 'gameStartShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
proxiwashWatchedMachines: {
|
||||
key: 'proxiwashWatchedMachines',
|
||||
default: '[]',
|
||||
},
|
||||
showAprilFoolsStart: {
|
||||
key: 'showAprilFoolsStart',
|
||||
default: '1',
|
||||
},
|
||||
planexCurrentGroup: {
|
||||
key: 'planexCurrentGroup',
|
||||
default: '',
|
||||
},
|
||||
planexFavoriteGroups: {
|
||||
key: 'planexFavoriteGroups',
|
||||
default: '[]',
|
||||
},
|
||||
dashboardItems: {
|
||||
key: 'dashboardItems',
|
||||
default: JSON.stringify([
|
||||
SERVICES_KEY.EMAIL,
|
||||
SERVICES_KEY.WASHERS,
|
||||
SERVICES_KEY.PROXIMO,
|
||||
SERVICES_KEY.TUTOR_INSA,
|
||||
SERVICES_KEY.RU,
|
||||
]),
|
||||
},
|
||||
gameScores: {
|
||||
key: 'gameScores',
|
||||
default: '[]',
|
||||
},
|
||||
};
|
||||
|
||||
static PREFERENCES = {
|
||||
debugUnlocked: {
|
||||
key: 'debugUnlocked',
|
||||
default: '0',
|
||||
},
|
||||
showIntro: {
|
||||
key: 'showIntro',
|
||||
default: '1',
|
||||
},
|
||||
updateNumber: {
|
||||
key: 'updateNumber',
|
||||
default: '0',
|
||||
},
|
||||
proxiwashNotifications: {
|
||||
key: 'proxiwashNotifications',
|
||||
default: '5',
|
||||
},
|
||||
nightModeFollowSystem: {
|
||||
key: 'nightModeFollowSystem',
|
||||
default: '1',
|
||||
},
|
||||
nightMode: {
|
||||
key: 'nightMode',
|
||||
default: '1',
|
||||
},
|
||||
defaultStartScreen: {
|
||||
key: 'defaultStartScreen',
|
||||
default: 'home',
|
||||
},
|
||||
servicesShowBanner: {
|
||||
key: 'servicesShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
proxiwashShowBanner: {
|
||||
key: 'proxiwashShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
homeShowBanner: {
|
||||
key: 'homeShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
eventsShowBanner: {
|
||||
key: 'eventsShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
planexShowBanner: {
|
||||
key: 'planexShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
loginShowBanner: {
|
||||
key: 'loginShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
voteShowBanner: {
|
||||
key: 'voteShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
equipmentShowBanner: {
|
||||
key: 'equipmentShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
gameStartShowBanner: {
|
||||
key: 'gameStartShowBanner',
|
||||
default: '1',
|
||||
},
|
||||
proxiwashWatchedMachines: {
|
||||
key: 'proxiwashWatchedMachines',
|
||||
default: '[]',
|
||||
},
|
||||
showAprilFoolsStart: {
|
||||
key: 'showAprilFoolsStart',
|
||||
default: '1',
|
||||
},
|
||||
planexCurrentGroup: {
|
||||
key: 'planexCurrentGroup',
|
||||
default: '',
|
||||
},
|
||||
planexFavoriteGroups: {
|
||||
key: 'planexFavoriteGroups',
|
||||
default: '[]',
|
||||
},
|
||||
dashboardItems: {
|
||||
key: 'dashboardItems',
|
||||
default: JSON.stringify([
|
||||
SERVICES_KEY.EMAIL,
|
||||
SERVICES_KEY.WASHERS,
|
||||
SERVICES_KEY.PROXIMO,
|
||||
SERVICES_KEY.TUTOR_INSA,
|
||||
SERVICES_KEY.RU,
|
||||
]),
|
||||
},
|
||||
gameScores: {
|
||||
key: 'gameScores',
|
||||
default: '[]',
|
||||
},
|
||||
}
|
||||
|
||||
#currentPreferences: {[key: string]: string};
|
||||
|
||||
constructor() {
|
||||
this.#currentPreferences = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {AsyncStorageManager}
|
||||
*/
|
||||
static getInstance(): AsyncStorageManager {
|
||||
return AsyncStorageManager.instance === null ?
|
||||
AsyncStorageManager.instance = new AsyncStorageManager() :
|
||||
AsyncStorageManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preferences object current values from AsyncStorage.
|
||||
* This function should be called at the app's start.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadPreferences() {
|
||||
let prefKeys = [];
|
||||
// Get all available keys
|
||||
for (let key in AsyncStorageManager.PREFERENCES) {
|
||||
prefKeys.push(key);
|
||||
}
|
||||
// Get corresponding values
|
||||
let resultArray: Array<Array<string>> = await AsyncStorage.multiGet(prefKeys);
|
||||
// Save those values for later use
|
||||
for (let i = 0; i < resultArray.length; i++) {
|
||||
let key: string = resultArray[i][0];
|
||||
let val: string | null = resultArray[i][1];
|
||||
if (val === null)
|
||||
val = AsyncStorageManager.PREFERENCES[key].default;
|
||||
this.#currentPreferences[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value associated to the given key to preferences.
|
||||
* This updates the preferences object and saves it to AsyncStorage.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
setPreference(key: string, value: any) {
|
||||
if (AsyncStorageManager.PREFERENCES[key] != null) {
|
||||
let convertedValue = "";
|
||||
if (typeof value === "string")
|
||||
convertedValue = value;
|
||||
else if (typeof value === "boolean" || typeof value === "number")
|
||||
convertedValue = value.toString();
|
||||
else
|
||||
convertedValue = JSON.stringify(value);
|
||||
this.#currentPreferences[key] = convertedValue;
|
||||
AsyncStorage.setItem(key, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value at the given key.
|
||||
* If the key is not available, returns null
|
||||
*
|
||||
* @param key
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getPreference(key: string) {
|
||||
return this.#currentPreferences[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* aves the value associated to the given key to preferences.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static set(key: string, value: any) {
|
||||
AsyncStorageManager.getInstance().setPreference(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static getString(key: string) {
|
||||
return AsyncStorageManager.getInstance().getPreference(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static getBool(key: string) {
|
||||
const value = AsyncStorageManager.getString(key);
|
||||
return value === "1" || value === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static getNumber(key: string) {
|
||||
return parseFloat(AsyncStorageManager.getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static getObject(key: string) {
|
||||
return JSON.parse(AsyncStorageManager.getString(key));
|
||||
#currentPreferences: {[key: string]: string};
|
||||
|
||||
constructor() {
|
||||
this.#currentPreferences = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {AsyncStorageManager}
|
||||
*/
|
||||
static getInstance(): AsyncStorageManager {
|
||||
if (AsyncStorageManager.instance == null)
|
||||
AsyncStorageManager.instance = new AsyncStorageManager();
|
||||
return AsyncStorageManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value associated to the given key to preferences.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static set(
|
||||
key: string,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
value: number | string | boolean | {...} | Array<any>,
|
||||
) {
|
||||
AsyncStorageManager.getInstance().setPreference(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {string}
|
||||
*/
|
||||
static getString(key: string): string {
|
||||
const value = AsyncStorageManager.getInstance().getPreference(key);
|
||||
return value != null ? value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static getBool(key: string): boolean {
|
||||
const value = AsyncStorageManager.getString(key);
|
||||
return value === '1' || value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {number}
|
||||
*/
|
||||
static getNumber(key: string): number {
|
||||
return parseFloat(AsyncStorageManager.getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the object value of the given preference
|
||||
*
|
||||
* @param key
|
||||
* @returns {{...}}
|
||||
*/
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
static getObject(key: string): any {
|
||||
return JSON.parse(AsyncStorageManager.getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preferences object current values from AsyncStorage.
|
||||
* This function should be called at the app's start.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadPreferences() {
|
||||
const prefKeys = [];
|
||||
// Get all available keys
|
||||
Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => {
|
||||
prefKeys.push(key);
|
||||
});
|
||||
// Get corresponding values
|
||||
const resultArray = await AsyncStorage.multiGet(prefKeys);
|
||||
// Save those values for later use
|
||||
resultArray.forEach((item: [string, string | null]) => {
|
||||
const key = item[0];
|
||||
let val = item[1];
|
||||
if (val === null) val = AsyncStorageManager.PREFERENCES[key].default;
|
||||
this.#currentPreferences[key] = val;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value associated to the given key to preferences.
|
||||
* This updates the preferences object and saves it to AsyncStorage.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
setPreference(
|
||||
key: string,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
value: number | string | boolean | {...} | Array<any>,
|
||||
) {
|
||||
if (AsyncStorageManager.PREFERENCES[key] != null) {
|
||||
let convertedValue;
|
||||
if (typeof value === 'string') convertedValue = value;
|
||||
else if (typeof value === 'boolean' || typeof value === 'number')
|
||||
convertedValue = value.toString();
|
||||
else convertedValue = JSON.stringify(value);
|
||||
this.#currentPreferences[key] = convertedValue;
|
||||
AsyncStorage.setItem(key, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value at the given key.
|
||||
* If the key is not available, returns null
|
||||
*
|
||||
* @param key
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getPreference(key: string): string | null {
|
||||
return this.#currentPreferences[key];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,63 +7,68 @@ import i18n from 'i18n-js';
|
|||
* Translations are hardcoded as toLocaleDateString does not work on current android JS engine
|
||||
*/
|
||||
export default class DateManager {
|
||||
static instance: DateManager | null = null;
|
||||
static instance: DateManager | null = null;
|
||||
|
||||
daysOfWeek = [];
|
||||
monthsOfYear = [];
|
||||
daysOfWeek = [];
|
||||
|
||||
constructor() {
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.sunday")); // 0 represents sunday
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.monday"));
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday"));
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.wednesday"));
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.thursday"));
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.friday"));
|
||||
this.daysOfWeek.push(i18n.t("date.daysOfWeek.saturday"));
|
||||
monthsOfYear = [];
|
||||
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.january"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.february"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.march"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.april"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.may"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.june"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.july"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.august"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.september"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.october"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.november"));
|
||||
this.monthsOfYear.push(i18n.t("date.monthsOfYear.december"));
|
||||
}
|
||||
constructor() {
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.monday'));
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.tuesday'));
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.wednesday'));
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.thursday'));
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.friday'));
|
||||
this.daysOfWeek.push(i18n.t('date.daysOfWeek.saturday'));
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {DateManager}
|
||||
*/
|
||||
static getInstance(): DateManager {
|
||||
return DateManager.instance === null ?
|
||||
DateManager.instance = new DateManager() :
|
||||
DateManager.instance;
|
||||
}
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.january'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.february'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.march'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.april'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.may'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.june'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.july'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.august'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.september'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.october'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.november'));
|
||||
this.monthsOfYear.push(i18n.t('date.monthsOfYear.december'));
|
||||
}
|
||||
|
||||
static isWeekend(date: Date) {
|
||||
return date.getDay() === 6 || date.getDay() === 0;
|
||||
}
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
* @returns {DateManager}
|
||||
*/
|
||||
static getInstance(): DateManager {
|
||||
if (DateManager.instance == null) DateManager.instance = new DateManager();
|
||||
return DateManager.instance;
|
||||
}
|
||||
|
||||
getMonthsOfYear() {
|
||||
return this.monthsOfYear;
|
||||
}
|
||||
static isWeekend(date: Date): boolean {
|
||||
return date.getDay() === 6 || date.getDay() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a translated string representing the given date.
|
||||
*
|
||||
* @param dateString The date with the format YYYY-MM-DD
|
||||
* @return {string} The translated string
|
||||
*/
|
||||
getTranslatedDate(dateString: string) {
|
||||
let dateArray = dateString.split('-');
|
||||
let date = new Date();
|
||||
date.setFullYear(parseInt(dateArray[0]), parseInt(dateArray[1]) - 1, parseInt(dateArray[2]));
|
||||
return this.daysOfWeek[date.getDay()] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
|
||||
}
|
||||
getMonthsOfYear(): Array<string> {
|
||||
return this.monthsOfYear;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a translated string representing the given date.
|
||||
*
|
||||
* @param dateString The date with the format YYYY-MM-DD
|
||||
* @return {string} The translated string
|
||||
*/
|
||||
getTranslatedDate(dateString: string): string {
|
||||
const dateArray = dateString.split('-');
|
||||
const date = new Date();
|
||||
date.setFullYear(
|
||||
parseInt(dateArray[0], 10),
|
||||
parseInt(dateArray[1], 10) - 1,
|
||||
parseInt(dateArray[2], 10),
|
||||
);
|
||||
return `${this.daysOfWeek[date.getDay()]} ${date.getDate()} ${
|
||||
this.monthsOfYear[date.getMonth()]
|
||||
} ${date.getFullYear()}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
// @flow
|
||||
|
||||
import i18n from 'i18n-js';
|
||||
import * as RNLocalize from "react-native-localize";
|
||||
import * as RNLocalize from 'react-native-localize';
|
||||
|
||||
import en from '../../locales/en';
|
||||
import en from '../../locales/en.json';
|
||||
import fr from '../../locales/fr.json';
|
||||
|
||||
/**
|
||||
* Static class used to manage locales
|
||||
*/
|
||||
export default class LocaleManager {
|
||||
|
||||
/**
|
||||
* Initialize translations using language files
|
||||
*/
|
||||
static initTranslations() {
|
||||
i18n.fallbacks = true;
|
||||
i18n.translations = {fr, en};
|
||||
i18n.locale = RNLocalize.findBestAvailableLanguage(["en", "fr"]).languageTag;
|
||||
}
|
||||
/**
|
||||
* Initialize translations using language files
|
||||
*/
|
||||
static initTranslations() {
|
||||
i18n.fallbacks = true;
|
||||
i18n.translations = {fr, en};
|
||||
i18n.locale = RNLocalize.findBestAvailableLanguage([
|
||||
'en',
|
||||
'fr',
|
||||
]).languageTag;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,282 +1,288 @@
|
|||
// @flow
|
||||
|
||||
import AsyncStorageManager from "./AsyncStorageManager";
|
||||
import {DarkTheme, DefaultTheme} from 'react-native-paper';
|
||||
import AprilFoolsManager from "./AprilFoolsManager";
|
||||
import {Appearance} from 'react-native-appearance';
|
||||
import AsyncStorageManager from './AsyncStorageManager';
|
||||
import AprilFoolsManager from './AprilFoolsManager';
|
||||
|
||||
const colorScheme = Appearance.getColorScheme();
|
||||
|
||||
export type CustomTheme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
primary: string,
|
||||
accent: string,
|
||||
tabIcon: string,
|
||||
card: string,
|
||||
dividerBackground: string,
|
||||
ripple: string,
|
||||
textDisabled: string,
|
||||
icon: string,
|
||||
subtitle: string,
|
||||
success: string,
|
||||
warning: string,
|
||||
danger: string,
|
||||
export type CustomThemeType = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
primary: string,
|
||||
accent: string,
|
||||
tabIcon: string,
|
||||
card: string,
|
||||
dividerBackground: string,
|
||||
ripple: string,
|
||||
textDisabled: string,
|
||||
icon: string,
|
||||
subtitle: string,
|
||||
success: string,
|
||||
warning: string,
|
||||
danger: string,
|
||||
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: string,
|
||||
agendaDayTextColor: string,
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: string,
|
||||
agendaDayTextColor: string,
|
||||
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: string,
|
||||
proxiwashReadyColor: string,
|
||||
proxiwashRunningColor: string,
|
||||
proxiwashRunningNotStartedColor: string,
|
||||
proxiwashRunningBgColor: string,
|
||||
proxiwashBrokenColor: string,
|
||||
proxiwashErrorColor: string,
|
||||
proxiwashUnknownColor: string,
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: string,
|
||||
proxiwashReadyColor: string,
|
||||
proxiwashRunningColor: string,
|
||||
proxiwashRunningNotStartedColor: string,
|
||||
proxiwashRunningBgColor: string,
|
||||
proxiwashBrokenColor: string,
|
||||
proxiwashErrorColor: string,
|
||||
proxiwashUnknownColor: string,
|
||||
|
||||
// Screens
|
||||
planningColor: string,
|
||||
proximoColor: string,
|
||||
proxiwashColor: string,
|
||||
menuColor: string,
|
||||
tutorinsaColor: string,
|
||||
// Screens
|
||||
planningColor: string,
|
||||
proximoColor: string,
|
||||
proxiwashColor: string,
|
||||
menuColor: string,
|
||||
tutorinsaColor: string,
|
||||
|
||||
// Tetris
|
||||
tetrisBackground: string,
|
||||
tetrisBorder: string,
|
||||
tetrisScore: string,
|
||||
tetrisI: string,
|
||||
tetrisO: string,
|
||||
tetrisT: string,
|
||||
tetrisS: string,
|
||||
tetrisZ: string,
|
||||
tetrisJ: string,
|
||||
tetrisL: string,
|
||||
// Tetris
|
||||
tetrisBackground: string,
|
||||
tetrisBorder: string,
|
||||
tetrisScore: string,
|
||||
tetrisI: string,
|
||||
tetrisO: string,
|
||||
tetrisT: string,
|
||||
tetrisS: string,
|
||||
tetrisZ: string,
|
||||
tetrisJ: string,
|
||||
tetrisL: string,
|
||||
|
||||
gameGold: string,
|
||||
gameSilver: string,
|
||||
gameBronze: string,
|
||||
gameGold: string,
|
||||
gameSilver: string,
|
||||
gameBronze: string,
|
||||
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: string,
|
||||
},
|
||||
}
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: string,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton class used to manage themes
|
||||
*/
|
||||
export default class ThemeManager {
|
||||
static instance: ThemeManager | null = null;
|
||||
|
||||
static instance: ThemeManager | null = null;
|
||||
updateThemeCallback: Function;
|
||||
updateThemeCallback: null | (() => void);
|
||||
|
||||
constructor() {
|
||||
this.updateThemeCallback = null;
|
||||
}
|
||||
constructor() {
|
||||
this.updateThemeCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the light theme
|
||||
*
|
||||
* @return {CustomTheme} Object containing theme variables
|
||||
* */
|
||||
static getWhiteTheme(): CustomTheme {
|
||||
return {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: '#be1522',
|
||||
accent: '#be1522',
|
||||
tabIcon: "#929292",
|
||||
card: "#fff",
|
||||
dividerBackground: '#e2e2e2',
|
||||
ripple: "rgba(0,0,0,0.2)",
|
||||
textDisabled: '#c1c1c1',
|
||||
icon: '#5d5d5d',
|
||||
subtitle: '#707070',
|
||||
success: "#5cb85c",
|
||||
warning: "#f0ad4e",
|
||||
danger: "#d9534f",
|
||||
cc: 'dst',
|
||||
/**
|
||||
* Gets the light theme
|
||||
*
|
||||
* @return {CustomThemeType} Object containing theme variables
|
||||
* */
|
||||
static getWhiteTheme(): CustomThemeType {
|
||||
return {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: '#be1522',
|
||||
accent: '#be1522',
|
||||
tabIcon: '#929292',
|
||||
card: '#fff',
|
||||
dividerBackground: '#e2e2e2',
|
||||
ripple: 'rgba(0,0,0,0.2)',
|
||||
textDisabled: '#c1c1c1',
|
||||
icon: '#5d5d5d',
|
||||
subtitle: '#707070',
|
||||
success: '#5cb85c',
|
||||
warning: '#f0ad4e',
|
||||
danger: '#d9534f',
|
||||
cc: 'dst',
|
||||
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#f3f3f4',
|
||||
agendaDayTextColor: '#636363',
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#f3f3f4',
|
||||
agendaDayTextColor: '#636363',
|
||||
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: "#a5dc9d",
|
||||
proxiwashReadyColor: "transparent",
|
||||
proxiwashRunningColor: "#a0ceff",
|
||||
proxiwashRunningNotStartedColor: "#c9e0ff",
|
||||
proxiwashRunningBgColor: "#c7e3ff",
|
||||
proxiwashBrokenColor: "#ffa8a2",
|
||||
proxiwashErrorColor: "#ffa8a2",
|
||||
proxiwashUnknownColor: "#b6b6b6",
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: '#a5dc9d',
|
||||
proxiwashReadyColor: 'transparent',
|
||||
proxiwashRunningColor: '#a0ceff',
|
||||
proxiwashRunningNotStartedColor: '#c9e0ff',
|
||||
proxiwashRunningBgColor: '#c7e3ff',
|
||||
proxiwashBrokenColor: '#ffa8a2',
|
||||
proxiwashErrorColor: '#ffa8a2',
|
||||
proxiwashUnknownColor: '#b6b6b6',
|
||||
|
||||
// Screens
|
||||
planningColor: '#d9b10a',
|
||||
proximoColor: '#ec5904',
|
||||
proxiwashColor: '#1fa5ee',
|
||||
menuColor: '#e91314',
|
||||
tutorinsaColor: '#f93943',
|
||||
// Screens
|
||||
planningColor: '#d9b10a',
|
||||
proximoColor: '#ec5904',
|
||||
proxiwashColor: '#1fa5ee',
|
||||
menuColor: '#e91314',
|
||||
tutorinsaColor: '#f93943',
|
||||
|
||||
// Tetris
|
||||
tetrisBackground: '#f0f0f0',
|
||||
tetrisScore: '#e2bd33',
|
||||
tetrisI: '#3cd9e6',
|
||||
tetrisO: '#ffdd00',
|
||||
tetrisT: '#a716e5',
|
||||
tetrisS: '#09c528',
|
||||
tetrisZ: '#ff0009',
|
||||
tetrisJ: '#2a67e3',
|
||||
tetrisL: '#da742d',
|
||||
// Tetris
|
||||
tetrisBackground: '#f0f0f0',
|
||||
tetrisScore: '#e2bd33',
|
||||
tetrisI: '#3cd9e6',
|
||||
tetrisO: '#ffdd00',
|
||||
tetrisT: '#a716e5',
|
||||
tetrisS: '#09c528',
|
||||
tetrisZ: '#ff0009',
|
||||
tetrisJ: '#2a67e3',
|
||||
tetrisL: '#da742d',
|
||||
|
||||
gameGold: "#ffd610",
|
||||
gameSilver: "#7b7b7b",
|
||||
gameBronze: "#a15218",
|
||||
gameGold: '#ffd610',
|
||||
gameSilver: '#7b7b7b',
|
||||
gameBronze: '#a15218',
|
||||
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: "#dedede",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: '#dedede',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dark theme
|
||||
*
|
||||
* @return {CustomTheme} Object containing theme variables
|
||||
* */
|
||||
static getDarkTheme(): CustomTheme {
|
||||
return {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: '#be1522',
|
||||
accent: '#be1522',
|
||||
tabBackground: "#181818",
|
||||
tabIcon: "#6d6d6d",
|
||||
card: "rgb(18,18,18)",
|
||||
dividerBackground: '#222222',
|
||||
ripple: "rgba(255,255,255,0.2)",
|
||||
textDisabled: '#5b5b5b',
|
||||
icon: '#b3b3b3',
|
||||
subtitle: '#aaaaaa',
|
||||
success: "#5cb85c",
|
||||
warning: "#f0ad4e",
|
||||
danger: "#d9534f",
|
||||
/**
|
||||
* Gets the dark theme
|
||||
*
|
||||
* @return {CustomThemeType} Object containing theme variables
|
||||
* */
|
||||
static getDarkTheme(): CustomThemeType {
|
||||
return {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: '#be1522',
|
||||
accent: '#be1522',
|
||||
tabBackground: '#181818',
|
||||
tabIcon: '#6d6d6d',
|
||||
card: 'rgb(18,18,18)',
|
||||
dividerBackground: '#222222',
|
||||
ripple: 'rgba(255,255,255,0.2)',
|
||||
textDisabled: '#5b5b5b',
|
||||
icon: '#b3b3b3',
|
||||
subtitle: '#aaaaaa',
|
||||
success: '#5cb85c',
|
||||
warning: '#f0ad4e',
|
||||
danger: '#d9534f',
|
||||
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#171717',
|
||||
agendaDayTextColor: '#6d6d6d',
|
||||
// Calendar/Agenda
|
||||
agendaBackgroundColor: '#171717',
|
||||
agendaDayTextColor: '#6d6d6d',
|
||||
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: "#31682c",
|
||||
proxiwashReadyColor: "transparent",
|
||||
proxiwashRunningColor: "#213c79",
|
||||
proxiwashRunningNotStartedColor: "#1e263e",
|
||||
proxiwashRunningBgColor: "#1a2033",
|
||||
proxiwashBrokenColor: "#7e2e2f",
|
||||
proxiwashErrorColor: "#7e2e2f",
|
||||
proxiwashUnknownColor: "#535353",
|
||||
// PROXIWASH
|
||||
proxiwashFinishedColor: '#31682c',
|
||||
proxiwashReadyColor: 'transparent',
|
||||
proxiwashRunningColor: '#213c79',
|
||||
proxiwashRunningNotStartedColor: '#1e263e',
|
||||
proxiwashRunningBgColor: '#1a2033',
|
||||
proxiwashBrokenColor: '#7e2e2f',
|
||||
proxiwashErrorColor: '#7e2e2f',
|
||||
proxiwashUnknownColor: '#535353',
|
||||
|
||||
// Screens
|
||||
planningColor: '#d99e09',
|
||||
proximoColor: '#ec5904',
|
||||
proxiwashColor: '#1fa5ee',
|
||||
menuColor: '#b81213',
|
||||
tutorinsaColor: '#f93943',
|
||||
// Screens
|
||||
planningColor: '#d99e09',
|
||||
proximoColor: '#ec5904',
|
||||
proxiwashColor: '#1fa5ee',
|
||||
menuColor: '#b81213',
|
||||
tutorinsaColor: '#f93943',
|
||||
|
||||
// Tetris
|
||||
tetrisBackground: '#181818',
|
||||
tetrisScore: '#e2d707',
|
||||
tetrisI: '#30b3be',
|
||||
tetrisO: '#c1a700',
|
||||
tetrisT: '#9114c7',
|
||||
tetrisS: '#08a121',
|
||||
tetrisZ: '#b50008',
|
||||
tetrisJ: '#0f37b9',
|
||||
tetrisL: '#b96226',
|
||||
// Tetris
|
||||
tetrisBackground: '#181818',
|
||||
tetrisScore: '#e2d707',
|
||||
tetrisI: '#30b3be',
|
||||
tetrisO: '#c1a700',
|
||||
tetrisT: '#9114c7',
|
||||
tetrisS: '#08a121',
|
||||
tetrisZ: '#b50008',
|
||||
tetrisJ: '#0f37b9',
|
||||
tetrisL: '#b96226',
|
||||
|
||||
gameGold: "#ffd610",
|
||||
gameSilver: "#7b7b7b",
|
||||
gameBronze: "#a15218",
|
||||
gameGold: '#ffd610',
|
||||
gameSilver: '#7b7b7b',
|
||||
gameBronze: '#a15218',
|
||||
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: "#323232",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Mascot Popup
|
||||
mascotMessageArrow: '#323232',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
*
|
||||
* @returns {ThemeManager}
|
||||
*/
|
||||
static getInstance(): ThemeManager {
|
||||
return ThemeManager.instance === null ?
|
||||
ThemeManager.instance = new ThemeManager() :
|
||||
ThemeManager.instance;
|
||||
}
|
||||
/**
|
||||
* Get this class instance or create one if none is found
|
||||
*
|
||||
* @returns {ThemeManager}
|
||||
*/
|
||||
static getInstance(): ThemeManager {
|
||||
if (ThemeManager.instance == null)
|
||||
ThemeManager.instance = new ThemeManager();
|
||||
return ThemeManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets night mode status.
|
||||
* If Follow System Preferences is enabled, will first use system theme.
|
||||
* If disabled or not available, will use value stored din preferences
|
||||
*
|
||||
* @returns {boolean} Night mode state
|
||||
*/
|
||||
static getNightMode(): boolean {
|
||||
return (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightMode.key) &&
|
||||
(!AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
|
||||
|| colorScheme === 'no-preference')) ||
|
||||
(AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
|
||||
&& colorScheme === 'dark');
|
||||
}
|
||||
/**
|
||||
* Gets night mode status.
|
||||
* If Follow System Preferences is enabled, will first use system theme.
|
||||
* If disabled or not available, will use value stored din preferences
|
||||
*
|
||||
* @returns {boolean} Night mode state
|
||||
*/
|
||||
static getNightMode(): boolean {
|
||||
return (
|
||||
(AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.nightMode.key,
|
||||
) &&
|
||||
(!AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||
) ||
|
||||
colorScheme === 'no-preference')) ||
|
||||
(AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||
) &&
|
||||
colorScheme === 'dark')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theme based on night mode and events
|
||||
*
|
||||
* @returns {CustomTheme} The current theme
|
||||
*/
|
||||
static getCurrentTheme(): CustomTheme {
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
|
||||
else
|
||||
return ThemeManager.getBaseTheme()
|
||||
}
|
||||
/**
|
||||
* Get the current theme based on night mode and events
|
||||
*
|
||||
* @returns {CustomThemeType} The current theme
|
||||
*/
|
||||
static getCurrentTheme(): CustomThemeType {
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
|
||||
return ThemeManager.getBaseTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the theme based on night mode
|
||||
*
|
||||
* @return {CustomTheme} The theme
|
||||
*/
|
||||
static getBaseTheme(): CustomTheme {
|
||||
if (ThemeManager.getNightMode())
|
||||
return ThemeManager.getDarkTheme();
|
||||
else
|
||||
return ThemeManager.getWhiteTheme();
|
||||
}
|
||||
/**
|
||||
* Get the theme based on night mode
|
||||
*
|
||||
* @return {CustomThemeType} The theme
|
||||
*/
|
||||
static getBaseTheme(): CustomThemeType {
|
||||
if (ThemeManager.getNightMode()) return ThemeManager.getDarkTheme();
|
||||
return ThemeManager.getWhiteTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the function to be called when the theme is changed (allows for general reload of the app)
|
||||
*
|
||||
* @param callback Function to call after theme change
|
||||
*/
|
||||
setUpdateThemeCallback(callback: () => void) {
|
||||
this.updateThemeCallback = callback;
|
||||
}
|
||||
/**
|
||||
* Sets the function to be called when the theme is changed (allows for general reload of the app)
|
||||
*
|
||||
* @param callback Function to call after theme change
|
||||
*/
|
||||
setUpdateThemeCallback(callback: () => void) {
|
||||
this.updateThemeCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set night mode and save it to preferences
|
||||
*
|
||||
* @param isNightMode True to enable night mode, false to disable
|
||||
*/
|
||||
setNightMode(isNightMode: boolean) {
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.nightMode.key, isNightMode);
|
||||
if (this.updateThemeCallback != null)
|
||||
this.updateThemeCallback();
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Set night mode and save it to preferences
|
||||
*
|
||||
* @param isNightMode True to enable night mode, false to disable
|
||||
*/
|
||||
setNightMode(isNightMode: boolean) {
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.nightMode.key,
|
||||
isNightMode,
|
||||
);
|
||||
if (this.updateThemeCallback != null) this.updateThemeCallback();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,204 +1,231 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
import {Platform} from 'react-native';
|
||||
import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
|
||||
import AboutScreen from '../screens/About/AboutScreen';
|
||||
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
||||
import DebugScreen from '../screens/About/DebugScreen';
|
||||
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
||||
import i18n from "i18n-js";
|
||||
import TabNavigator from "./TabNavigator";
|
||||
import GameMainScreen from "../screens/Game/screens/GameMainScreen";
|
||||
import VoteScreen from "../screens/Amicale/VoteScreen";
|
||||
import LoginScreen from "../screens/Amicale/LoginScreen";
|
||||
import {Platform} from "react-native";
|
||||
import SelfMenuScreen from "../screens/Services/SelfMenuScreen";
|
||||
import ProximoMainScreen from "../screens/Services/Proximo/ProximoMainScreen";
|
||||
import ProximoListScreen from "../screens/Services/Proximo/ProximoListScreen";
|
||||
import ProximoAboutScreen from "../screens/Services/Proximo/ProximoAboutScreen";
|
||||
import ProfileScreen from "../screens/Amicale/ProfileScreen";
|
||||
import ClubListScreen from "../screens/Amicale/Clubs/ClubListScreen";
|
||||
import ClubAboutScreen from "../screens/Amicale/Clubs/ClubAboutScreen";
|
||||
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
|
||||
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
|
||||
import BugReportScreen from "../screens/Other/FeedbackScreen";
|
||||
import WebsiteScreen from "../screens/Services/WebsiteScreen";
|
||||
import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
|
||||
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
|
||||
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
|
||||
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
|
||||
import GameStartScreen from "../screens/Game/screens/GameStartScreen";
|
||||
import TabNavigator from './TabNavigator';
|
||||
import GameMainScreen from '../screens/Game/screens/GameMainScreen';
|
||||
import VoteScreen from '../screens/Amicale/VoteScreen';
|
||||
import LoginScreen from '../screens/Amicale/LoginScreen';
|
||||
import SelfMenuScreen from '../screens/Services/SelfMenuScreen';
|
||||
import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen';
|
||||
import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen';
|
||||
import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen';
|
||||
import ProfileScreen from '../screens/Amicale/ProfileScreen';
|
||||
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
|
||||
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
|
||||
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||
import {
|
||||
createScreenCollapsibleStack,
|
||||
getWebsiteStack,
|
||||
} from '../utils/CollapsibleUtils';
|
||||
import BugReportScreen from '../screens/Other/FeedbackScreen';
|
||||
import WebsiteScreen from '../screens/Services/WebsiteScreen';
|
||||
import EquipmentScreen from '../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen';
|
||||
import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen';
|
||||
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
|
||||
import GameStartScreen from '../screens/Game/screens/GameStartScreen';
|
||||
|
||||
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
|
||||
const modalTransition =
|
||||
Platform.OS === 'ios'
|
||||
? TransitionPresets.ModalPresentationIOS
|
||||
: TransitionPresets.ModalSlideFromBottomIOS;
|
||||
|
||||
const defaultScreenOptions = {
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
};
|
||||
|
||||
|
||||
const MainStack = createStackNavigator();
|
||||
|
||||
function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
|
||||
return (
|
||||
<MainStack.Navigator
|
||||
initialRouteName={'main'}
|
||||
headerMode={'screen'}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
<MainStack.Screen
|
||||
name="main"
|
||||
component={props.createTabNavigator}
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: i18n.t('screens.home.title'),
|
||||
}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
"settings",
|
||||
MainStack,
|
||||
SettingsScreen,
|
||||
i18n.t('screens.settings.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"dashboard-edit",
|
||||
MainStack,
|
||||
DashboardEditScreen,
|
||||
i18n.t('screens.settings.dashboardEdit.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"about",
|
||||
MainStack,
|
||||
AboutScreen,
|
||||
i18n.t('screens.about.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"dependencies",
|
||||
MainStack,
|
||||
AboutDependenciesScreen,
|
||||
i18n.t('screens.about.libs'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"debug",
|
||||
MainStack,
|
||||
DebugScreen,
|
||||
i18n.t('screens.about.debug'))}
|
||||
function MainStackComponent(props: {
|
||||
createTabNavigator: () => React.Node,
|
||||
}): React.Node {
|
||||
const {createTabNavigator} = props;
|
||||
return (
|
||||
<MainStack.Navigator
|
||||
initialRouteName="main"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
<MainStack.Screen
|
||||
name="main"
|
||||
component={createTabNavigator}
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: i18n.t('screens.home.title'),
|
||||
}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
'settings',
|
||||
MainStack,
|
||||
SettingsScreen,
|
||||
i18n.t('screens.settings.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'dashboard-edit',
|
||||
MainStack,
|
||||
DashboardEditScreen,
|
||||
i18n.t('screens.settings.dashboardEdit.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'about',
|
||||
MainStack,
|
||||
AboutScreen,
|
||||
i18n.t('screens.about.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'dependencies',
|
||||
MainStack,
|
||||
AboutDependenciesScreen,
|
||||
i18n.t('screens.about.libs'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'debug',
|
||||
MainStack,
|
||||
DebugScreen,
|
||||
i18n.t('screens.about.debug'),
|
||||
)}
|
||||
|
||||
{createScreenCollapsibleStack(
|
||||
"game-start",
|
||||
MainStack,
|
||||
GameStartScreen,
|
||||
i18n.t('screens.game.title'))}
|
||||
<MainStack.Screen
|
||||
name="game-main"
|
||||
component={GameMainScreen}
|
||||
options={{
|
||||
title: i18n.t("screens.game.title"),
|
||||
}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
"login",
|
||||
MainStack,
|
||||
LoginScreen,
|
||||
i18n.t('screens.login.title'),
|
||||
true,
|
||||
{headerTintColor: "#fff"},
|
||||
'transparent')}
|
||||
{getWebsiteStack("website", MainStack, WebsiteScreen, "")}
|
||||
{createScreenCollapsibleStack(
|
||||
'game-start',
|
||||
MainStack,
|
||||
GameStartScreen,
|
||||
i18n.t('screens.game.title'),
|
||||
)}
|
||||
<MainStack.Screen
|
||||
name="game-main"
|
||||
component={GameMainScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.game.title'),
|
||||
}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
'login',
|
||||
MainStack,
|
||||
LoginScreen,
|
||||
i18n.t('screens.login.title'),
|
||||
true,
|
||||
{headerTintColor: '#fff'},
|
||||
'transparent',
|
||||
)}
|
||||
{getWebsiteStack('website', MainStack, WebsiteScreen, '')}
|
||||
|
||||
{createScreenCollapsibleStack(
|
||||
'self-menu',
|
||||
MainStack,
|
||||
SelfMenuScreen,
|
||||
i18n.t('screens.menu.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'proximo',
|
||||
MainStack,
|
||||
ProximoMainScreen,
|
||||
i18n.t('screens.proximo.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'proximo-list',
|
||||
MainStack,
|
||||
ProximoListScreen,
|
||||
i18n.t('screens.proximo.articleList'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'proximo-about',
|
||||
MainStack,
|
||||
ProximoAboutScreen,
|
||||
i18n.t('screens.proximo.title'),
|
||||
true,
|
||||
{...modalTransition},
|
||||
)}
|
||||
|
||||
{createScreenCollapsibleStack(
|
||||
"self-menu",
|
||||
MainStack,
|
||||
SelfMenuScreen,
|
||||
i18n.t('screens.menu.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"proximo",
|
||||
MainStack,
|
||||
ProximoMainScreen,
|
||||
i18n.t('screens.proximo.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"proximo-list",
|
||||
MainStack,
|
||||
ProximoListScreen,
|
||||
i18n.t('screens.proximo.articleList'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
"proximo-about",
|
||||
MainStack,
|
||||
ProximoAboutScreen,
|
||||
i18n.t('screens.proximo.title'),
|
||||
true,
|
||||
{...modalTransition},
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'profile',
|
||||
MainStack,
|
||||
ProfileScreen,
|
||||
i18n.t('screens.profile.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'club-list',
|
||||
MainStack,
|
||||
ClubListScreen,
|
||||
i18n.t('screens.clubs.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'club-information',
|
||||
MainStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details'),
|
||||
true,
|
||||
{...modalTransition},
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'club-about',
|
||||
MainStack,
|
||||
ClubAboutScreen,
|
||||
i18n.t('screens.clubs.title'),
|
||||
true,
|
||||
{...modalTransition},
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'equipment-list',
|
||||
MainStack,
|
||||
EquipmentScreen,
|
||||
i18n.t('screens.equipment.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'equipment-rent',
|
||||
MainStack,
|
||||
EquipmentLendScreen,
|
||||
i18n.t('screens.equipment.book'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'equipment-confirm',
|
||||
MainStack,
|
||||
EquipmentConfirmScreen,
|
||||
i18n.t('screens.equipment.confirm'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'vote',
|
||||
MainStack,
|
||||
VoteScreen,
|
||||
i18n.t('screens.vote.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'feedback',
|
||||
MainStack,
|
||||
BugReportScreen,
|
||||
i18n.t('screens.feedback.title'),
|
||||
)}
|
||||
</MainStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
{createScreenCollapsibleStack(
|
||||
"profile",
|
||||
MainStack,
|
||||
ProfileScreen,
|
||||
i18n.t('screens.profile.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"club-list",
|
||||
MainStack,
|
||||
ClubListScreen,
|
||||
i18n.t('screens.clubs.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"club-information",
|
||||
MainStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details'),
|
||||
true,
|
||||
{...modalTransition})}
|
||||
{createScreenCollapsibleStack(
|
||||
"club-about",
|
||||
MainStack,
|
||||
ClubAboutScreen,
|
||||
i18n.t('screens.clubs.title'),
|
||||
true,
|
||||
{...modalTransition})}
|
||||
{createScreenCollapsibleStack(
|
||||
"equipment-list",
|
||||
MainStack,
|
||||
EquipmentScreen,
|
||||
i18n.t('screens.equipment.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"equipment-rent",
|
||||
MainStack,
|
||||
EquipmentLendScreen,
|
||||
i18n.t('screens.equipment.book'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"equipment-confirm",
|
||||
MainStack,
|
||||
EquipmentConfirmScreen,
|
||||
i18n.t('screens.equipment.confirm'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"vote",
|
||||
MainStack,
|
||||
VoteScreen,
|
||||
i18n.t('screens.vote.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"feedback",
|
||||
MainStack,
|
||||
BugReportScreen,
|
||||
i18n.t('screens.feedback.title'))}
|
||||
</MainStack.Navigator>
|
||||
type PropsType = {
|
||||
defaultHomeRoute: string | null,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
defaultHomeData: {[key: string]: string},
|
||||
};
|
||||
|
||||
export default class MainNavigator extends React.Component<PropsType> {
|
||||
createTabNavigator: () => React.Node;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.createTabNavigator = (): React.Node => (
|
||||
<TabNavigator
|
||||
defaultHomeRoute={props.defaultHomeRoute}
|
||||
defaultHomeData={props.defaultHomeData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
defaultHomeRoute: string | null,
|
||||
defaultHomeData: { [key: string]: any }
|
||||
}
|
||||
|
||||
export default class MainNavigator extends React.Component<Props> {
|
||||
|
||||
createTabNavigator: () => React.Node;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.createTabNavigator = () => <TabNavigator {...props}/>
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MainStackComponent createTabNavigator={this.createTabNavigator}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
return <MainStackComponent createTabNavigator={this.createTabNavigator} />;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,271 +1,292 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
|
||||
|
||||
import {Title, useTheme} from 'react-native-paper';
|
||||
import {Platform} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {createCollapsibleStack} from 'react-navigation-collapsible';
|
||||
import {View} from 'react-native-animatable';
|
||||
import HomeScreen from '../screens/Home/HomeScreen';
|
||||
import PlanningScreen from '../screens/Planning/PlanningScreen';
|
||||
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
|
||||
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
|
||||
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
|
||||
import PlanexScreen from '../screens/Planex/PlanexScreen';
|
||||
import AsyncStorageManager from "../managers/AsyncStorageManager";
|
||||
import {Title, useTheme} from 'react-native-paper';
|
||||
import {Platform} from 'react-native';
|
||||
import i18n from "i18n-js";
|
||||
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
|
||||
import ScannerScreen from "../screens/Home/ScannerScreen";
|
||||
import FeedItemScreen from "../screens/Home/FeedItemScreen";
|
||||
import {createCollapsibleStack} from "react-navigation-collapsible";
|
||||
import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen";
|
||||
import CustomTabBar from "../components/Tabbar/CustomTabBar";
|
||||
import WebsitesHomeScreen from "../screens/Services/ServicesScreen";
|
||||
import ServicesSectionScreen from "../screens/Services/ServicesSectionScreen";
|
||||
import AmicaleContactScreen from "../screens/Amicale/AmicaleContactScreen";
|
||||
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
|
||||
import {View} from "react-native-animatable";
|
||||
import Mascot, {MASCOT_STYLE} from "../components/Mascot/Mascot";
|
||||
|
||||
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
|
||||
import AsyncStorageManager from '../managers/AsyncStorageManager';
|
||||
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||
import ScannerScreen from '../screens/Home/ScannerScreen';
|
||||
import FeedItemScreen from '../screens/Home/FeedItemScreen';
|
||||
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen';
|
||||
import CustomTabBar from '../components/Tabbar/CustomTabBar';
|
||||
import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
|
||||
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
|
||||
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
|
||||
import {
|
||||
createScreenCollapsibleStack,
|
||||
getWebsiteStack,
|
||||
} from '../utils/CollapsibleUtils';
|
||||
import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot';
|
||||
|
||||
const modalTransition =
|
||||
Platform.OS === 'ios'
|
||||
? TransitionPresets.ModalPresentationIOS
|
||||
: TransitionPresets.ModalSlideFromBottomIOS;
|
||||
|
||||
const defaultScreenOptions = {
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...modalTransition,
|
||||
gestureEnabled: true,
|
||||
cardOverlayEnabled: true,
|
||||
...modalTransition,
|
||||
};
|
||||
|
||||
|
||||
const ServicesStack = createStackNavigator();
|
||||
|
||||
function ServicesStackComponent() {
|
||||
return (
|
||||
<ServicesStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode={"screen"}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{createScreenCollapsibleStack(
|
||||
"index",
|
||||
ServicesStack,
|
||||
WebsitesHomeScreen,
|
||||
i18n.t('screens.services.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"services-section",
|
||||
ServicesStack,
|
||||
ServicesSectionScreen,
|
||||
"SECTION")}
|
||||
{createScreenCollapsibleStack(
|
||||
"amicale-contact",
|
||||
ServicesStack,
|
||||
AmicaleContactScreen,
|
||||
i18n.t('screens.amicaleAbout.title'))}
|
||||
</ServicesStack.Navigator>
|
||||
);
|
||||
function ServicesStackComponent(): React.Node {
|
||||
return (
|
||||
<ServicesStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
{createScreenCollapsibleStack(
|
||||
'index',
|
||||
ServicesStack,
|
||||
WebsitesHomeScreen,
|
||||
i18n.t('screens.services.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'services-section',
|
||||
ServicesStack,
|
||||
ServicesSectionScreen,
|
||||
'SECTION',
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'amicale-contact',
|
||||
ServicesStack,
|
||||
AmicaleContactScreen,
|
||||
i18n.t('screens.amicaleAbout.title'),
|
||||
)}
|
||||
</ServicesStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const ProxiwashStack = createStackNavigator();
|
||||
|
||||
function ProxiwashStackComponent() {
|
||||
return (
|
||||
<ProxiwashStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode={"screen"}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{createScreenCollapsibleStack(
|
||||
"index",
|
||||
ProxiwashStack,
|
||||
ProxiwashScreen,
|
||||
i18n.t('screens.proxiwash.title'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"proxiwash-about",
|
||||
ProxiwashStack,
|
||||
ProxiwashAboutScreen,
|
||||
i18n.t('screens.proxiwash.title'))}
|
||||
</ProxiwashStack.Navigator>
|
||||
);
|
||||
function ProxiwashStackComponent(): React.Node {
|
||||
return (
|
||||
<ProxiwashStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
{createScreenCollapsibleStack(
|
||||
'index',
|
||||
ProxiwashStack,
|
||||
ProxiwashScreen,
|
||||
i18n.t('screens.proxiwash.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'proxiwash-about',
|
||||
ProxiwashStack,
|
||||
ProxiwashAboutScreen,
|
||||
i18n.t('screens.proxiwash.title'),
|
||||
)}
|
||||
</ProxiwashStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const PlanningStack = createStackNavigator();
|
||||
|
||||
function PlanningStackComponent() {
|
||||
return (
|
||||
<PlanningStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode={"screen"}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
<PlanningStack.Screen
|
||||
name="index"
|
||||
component={PlanningScreen}
|
||||
options={{title: i18n.t('screens.planning.title'),}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
"planning-information",
|
||||
PlanningStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails'))}
|
||||
</PlanningStack.Navigator>
|
||||
);
|
||||
function PlanningStackComponent(): React.Node {
|
||||
return (
|
||||
<PlanningStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
<PlanningStack.Screen
|
||||
name="index"
|
||||
component={PlanningScreen}
|
||||
options={{title: i18n.t('screens.planning.title')}}
|
||||
/>
|
||||
{createScreenCollapsibleStack(
|
||||
'planning-information',
|
||||
PlanningStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails'),
|
||||
)}
|
||||
</PlanningStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const HomeStack = createStackNavigator();
|
||||
|
||||
function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) {
|
||||
let params = undefined;
|
||||
if (initialRoute != null)
|
||||
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
|
||||
const {colors} = useTheme();
|
||||
return (
|
||||
<HomeStack.Navigator
|
||||
initialRouteName={"index"}
|
||||
headerMode={"screen"}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{createCollapsibleStack(
|
||||
<HomeStack.Screen
|
||||
name="index"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.home.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
headerTitle: () =>
|
||||
<View style={{flexDirection: "row"}}>
|
||||
<Mascot
|
||||
style={{
|
||||
width: 50
|
||||
}}
|
||||
emotion={MASCOT_STYLE.RANDOM}
|
||||
animated={true}
|
||||
entryAnimation={{
|
||||
animation: "bounceIn",
|
||||
duration: 1000
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: "pulse",
|
||||
duration: 2000,
|
||||
iterationCount: "infinite"
|
||||
}}
|
||||
/>
|
||||
<Title style={{
|
||||
marginLeft: 10,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}>{i18n.t('screens.home.title')}</Title>
|
||||
</View>
|
||||
}}
|
||||
initialParams={params}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: colors.surface,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
)}
|
||||
<HomeStack.Screen
|
||||
name="scanner"
|
||||
component={ScannerScreen}
|
||||
options={{title: i18n.t('screens.scanner.title'),}}
|
||||
/>
|
||||
function HomeStackComponent(
|
||||
initialRoute: string | null,
|
||||
defaultData: {[key: string]: string},
|
||||
): React.Node {
|
||||
let params;
|
||||
if (initialRoute != null)
|
||||
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
|
||||
const {colors} = useTheme();
|
||||
return (
|
||||
<HomeStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
{createCollapsibleStack(
|
||||
<HomeStack.Screen
|
||||
name="index"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.home.title'),
|
||||
headerStyle: {
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
headerTitle: (): React.Node => (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Mascot
|
||||
style={{
|
||||
width: 50,
|
||||
}}
|
||||
emotion={MASCOT_STYLE.RANDOM}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
loopAnimation={{
|
||||
animation: 'pulse',
|
||||
duration: 2000,
|
||||
iterationCount: 'infinite',
|
||||
}}
|
||||
/>
|
||||
<Title
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
{i18n.t('screens.home.title')}
|
||||
</Title>
|
||||
</View>
|
||||
),
|
||||
}}
|
||||
initialParams={params}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: colors.surface,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
)}
|
||||
<HomeStack.Screen
|
||||
name="scanner"
|
||||
component={ScannerScreen}
|
||||
options={{title: i18n.t('screens.scanner.title')}}
|
||||
/>
|
||||
|
||||
{createScreenCollapsibleStack(
|
||||
"club-information",
|
||||
HomeStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"feed-information",
|
||||
HomeStack,
|
||||
FeedItemScreen,
|
||||
i18n.t('screens.home.feed'))}
|
||||
{createScreenCollapsibleStack(
|
||||
"planning-information",
|
||||
HomeStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails'))}
|
||||
</HomeStack.Navigator>
|
||||
);
|
||||
{createScreenCollapsibleStack(
|
||||
'club-information',
|
||||
HomeStack,
|
||||
ClubDisplayScreen,
|
||||
i18n.t('screens.clubs.details'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'feed-information',
|
||||
HomeStack,
|
||||
FeedItemScreen,
|
||||
i18n.t('screens.home.feed'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'planning-information',
|
||||
HomeStack,
|
||||
PlanningDisplayScreen,
|
||||
i18n.t('screens.planning.eventDetails'),
|
||||
)}
|
||||
</HomeStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const PlanexStack = createStackNavigator();
|
||||
|
||||
function PlanexStackComponent() {
|
||||
return (
|
||||
<PlanexStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode={"screen"}
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{getWebsiteStack(
|
||||
"index",
|
||||
PlanexStack,
|
||||
PlanexScreen,
|
||||
i18n.t("screens.planex.title"))}
|
||||
{createScreenCollapsibleStack(
|
||||
"group-select",
|
||||
PlanexStack,
|
||||
GroupSelectionScreen,
|
||||
"")}
|
||||
</PlanexStack.Navigator>
|
||||
);
|
||||
function PlanexStackComponent(): React.Node {
|
||||
return (
|
||||
<PlanexStack.Navigator
|
||||
initialRouteName="index"
|
||||
headerMode="screen"
|
||||
screenOptions={defaultScreenOptions}>
|
||||
{getWebsiteStack(
|
||||
'index',
|
||||
PlanexStack,
|
||||
PlanexScreen,
|
||||
i18n.t('screens.planex.title'),
|
||||
)}
|
||||
{createScreenCollapsibleStack(
|
||||
'group-select',
|
||||
PlanexStack,
|
||||
GroupSelectionScreen,
|
||||
'',
|
||||
)}
|
||||
</PlanexStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
type Props = {
|
||||
defaultHomeRoute: string | null,
|
||||
defaultHomeData: { [key: string]: any }
|
||||
}
|
||||
|
||||
export default class TabNavigator extends React.Component<Props> {
|
||||
|
||||
createHomeStackComponent: () => HomeStackComponent;
|
||||
defaultRoute: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (props.defaultHomeRoute != null)
|
||||
this.defaultRoute = 'home';
|
||||
else
|
||||
this.defaultRoute = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key).toLowerCase();
|
||||
this.createHomeStackComponent = () => HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName={this.defaultRoute}
|
||||
tabBar={props => <CustomTabBar {...props} />}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="services"
|
||||
option
|
||||
component={ServicesStackComponent}
|
||||
options={{title: i18n.t('screens.services.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="proxiwash"
|
||||
component={ProxiwashStackComponent}
|
||||
options={{title: i18n.t('screens.proxiwash.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="home"
|
||||
component={this.createHomeStackComponent}
|
||||
options={{title: i18n.t('screens.home.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="planning"
|
||||
component={PlanningStackComponent}
|
||||
options={{title: i18n.t('screens.planning.title')}}
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
name="planex"
|
||||
component={PlanexStackComponent}
|
||||
options={{title: i18n.t("screens.planex.title")}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
type PropsType = {
|
||||
defaultHomeRoute: string | null,
|
||||
defaultHomeData: {[key: string]: string},
|
||||
};
|
||||
|
||||
export default class TabNavigator extends React.Component<PropsType> {
|
||||
createHomeStackComponent: () => React.Node;
|
||||
|
||||
defaultRoute: string;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
if (props.defaultHomeRoute != null) this.defaultRoute = 'home';
|
||||
else
|
||||
this.defaultRoute = AsyncStorageManager.getString(
|
||||
AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
|
||||
).toLowerCase();
|
||||
this.createHomeStackComponent = (): React.Node =>
|
||||
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName={this.defaultRoute}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}>
|
||||
<Tab.Screen
|
||||
name="services"
|
||||
option
|
||||
component={ServicesStackComponent}
|
||||
options={{title: i18n.t('screens.services.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="proxiwash"
|
||||
component={ProxiwashStackComponent}
|
||||
options={{title: i18n.t('screens.proxiwash.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="home"
|
||||
component={this.createHomeStackComponent}
|
||||
options={{title: i18n.t('screens.home.title')}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="planning"
|
||||
component={PlanningStackComponent}
|
||||
options={{title: i18n.t('screens.planning.title')}}
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
name="planex"
|
||||
component={PlanexStackComponent}
|
||||
options={{title: i18n.t('screens.planex.title')}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,31 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import packageJson from '../../../package';
|
||||
import {List} from 'react-native-paper';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
||||
import {View} from "react-native-animatable";
|
||||
import {View} from 'react-native-animatable';
|
||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
type listItem = {
|
||||
name: string,
|
||||
version: string
|
||||
type ListItemType = {
|
||||
name: string,
|
||||
version: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the dependencies list from the raw json
|
||||
*
|
||||
* @param object The raw json
|
||||
* @return {Array<listItem>}
|
||||
* @return {Array<ListItemType>}
|
||||
*/
|
||||
function generateListFromObject(object: { [key: string]: string }): Array<listItem> {
|
||||
let list = [];
|
||||
let keys = Object.keys(object);
|
||||
let values = Object.values(object);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
list.push({name: keys[i], version: values[i]});
|
||||
}
|
||||
//$FlowFixMe
|
||||
return list;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
function generateListFromObject(object: {
|
||||
[key: string]: string,
|
||||
}): Array<ListItemType> {
|
||||
const list = [];
|
||||
const keys = Object.keys(object);
|
||||
keys.forEach((key: string) => {
|
||||
list.push({name: key, version: object[key]});
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
|
@ -38,38 +33,45 @@ const LIST_ITEM_HEIGHT = 64;
|
|||
/**
|
||||
* Class defining a screen showing the list of libraries used by the app, taken from package.json
|
||||
*/
|
||||
export default class AboutDependenciesScreen extends React.Component<Props> {
|
||||
export default class AboutDependenciesScreen extends React.Component<null> {
|
||||
data: Array<ListItemType>;
|
||||
|
||||
data: Array<listItem>;
|
||||
constructor() {
|
||||
super();
|
||||
this.data = generateListFromObject(packageJson.dependencies);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.data = generateListFromObject(packageJson.dependencies);
|
||||
}
|
||||
keyExtractor = (item: ListItemType): string => item.name;
|
||||
|
||||
keyExtractor = (item: listItem) => item.name;
|
||||
getRenderItem = ({item}: {item: ListItemType}): React.Node => (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={item.version.replace('^', '').replace('~', '')}
|
||||
style={{height: LIST_ITEM_HEIGHT}}
|
||||
/>
|
||||
);
|
||||
|
||||
renderItem = ({item}: { item: listItem }) =>
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={item.version.replace('^', '').replace('~', '')}
|
||||
style={{height: LIST_ITEM_HEIGHT}}
|
||||
/>;
|
||||
getItemLayout = (
|
||||
data: ListItemType,
|
||||
index: number,
|
||||
): {length: number, offset: number, index: number} => ({
|
||||
length: LIST_ITEM_HEIGHT,
|
||||
offset: LIST_ITEM_HEIGHT * index,
|
||||
index,
|
||||
});
|
||||
|
||||
itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<CollapsibleFlatList
|
||||
data={this.data}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
removeClippedSubviews={true}
|
||||
getItemLayout={this.itemLayout}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
return (
|
||||
<View>
|
||||
<CollapsibleFlatList
|
||||
data={this.data}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
removeClippedSubviews
|
||||
getItemLayout={this.getItemLayout}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,351 +1,388 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {FlatList, Linking, Platform, View} from 'react-native';
|
||||
import i18n from "i18n-js";
|
||||
import {FlatList, Linking, Platform} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
|
||||
import packageJson from "../../../package.json";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import packageJson from '../../../package.json';
|
||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||
import APP_LOGO from '../../../assets/android.icon.png';
|
||||
|
||||
type ListItem = {
|
||||
onPressCallback: () => void,
|
||||
icon: string,
|
||||
text: string,
|
||||
showChevron: boolean
|
||||
type ListItemType = {
|
||||
onPressCallback: () => void,
|
||||
icon: string,
|
||||
text: string,
|
||||
showChevron: boolean,
|
||||
};
|
||||
|
||||
const links = {
|
||||
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
|
||||
playstore: 'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
|
||||
git: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md',
|
||||
changelog: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
|
||||
license: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
|
||||
authorMail: "mailto:vergnet@etud.insa-toulouse.fr?" +
|
||||
"subject=" +
|
||||
"Application Amicale INSA Toulouse" +
|
||||
"&body=" +
|
||||
"Coucou !\n\n",
|
||||
authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/',
|
||||
yohanMail: "mailto:ysimard@etud.insa-toulouse.fr?" +
|
||||
"subject=" +
|
||||
"Application Amicale INSA Toulouse" +
|
||||
"&body=" +
|
||||
"Coucou !\n\n",
|
||||
yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard',
|
||||
react: 'https://facebook.github.io/react-native/',
|
||||
meme: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
|
||||
playstore:
|
||||
'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
|
||||
git:
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md',
|
||||
changelog:
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
|
||||
license:
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
|
||||
authorMail:
|
||||
'mailto:vergnet@etud.insa-toulouse.fr?' +
|
||||
'subject=' +
|
||||
'Application Amicale INSA Toulouse' +
|
||||
'&body=' +
|
||||
'Coucou !\n\n',
|
||||
authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/',
|
||||
yohanMail:
|
||||
'mailto:ysimard@etud.insa-toulouse.fr?' +
|
||||
'subject=' +
|
||||
'Application Amicale INSA Toulouse' +
|
||||
'&body=' +
|
||||
'Coucou !\n\n',
|
||||
yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard',
|
||||
react: 'https://facebook.github.io/react-native/',
|
||||
meme: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens a link in the device's browser
|
||||
* @param link The link to open
|
||||
*/
|
||||
function openWebLink(link) {
|
||||
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
|
||||
function openWebLink(link: string) {
|
||||
Linking.openURL(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class defining an about screen. This screen shows the user information about the app and it's author.
|
||||
*/
|
||||
class AboutScreen extends React.Component<Props> {
|
||||
class AboutScreen extends React.Component<PropsType> {
|
||||
/**
|
||||
* Data to be displayed in the app card
|
||||
*/
|
||||
appData = [
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(Platform.OS === 'ios' ? links.appstore : links.playstore);
|
||||
},
|
||||
icon: Platform.OS === 'ios' ? 'apple' : 'google-play',
|
||||
text:
|
||||
Platform.OS === 'ios'
|
||||
? i18n.t('screens.about.appstore')
|
||||
: i18n.t('screens.about.playstore'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
const {navigation} = this.props;
|
||||
navigation.navigate('feedback');
|
||||
},
|
||||
icon: 'bug',
|
||||
text: i18n.t('screens.feedback.homeButtonTitle'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.git);
|
||||
},
|
||||
icon: 'git',
|
||||
text: 'Git',
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.changelog);
|
||||
},
|
||||
icon: 'refresh',
|
||||
text: i18n.t('screens.about.changelog'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.license);
|
||||
},
|
||||
icon: 'file-document',
|
||||
text: i18n.t('screens.about.license'),
|
||||
showChevron: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Data to be displayed in the app card
|
||||
*/
|
||||
appData = [
|
||||
{
|
||||
onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore),
|
||||
icon: Platform.OS === "ios" ? 'apple' : 'google-play',
|
||||
text: Platform.OS === "ios" ? i18n.t('screens.about.appstore') : i18n.t('screens.about.playstore'),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => this.props.navigation.navigate("feedback"),
|
||||
icon: 'bug',
|
||||
text: i18n.t("screens.feedback.homeButtonTitle"),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.git),
|
||||
icon: 'git',
|
||||
text: 'Git',
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.changelog),
|
||||
icon: 'refresh',
|
||||
text: i18n.t('screens.about.changelog'),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.license),
|
||||
icon: 'file-document',
|
||||
text: i18n.t('screens.about.license'),
|
||||
showChevron: true
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Data to be displayed in the author card
|
||||
*/
|
||||
authorData = [
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.meme),
|
||||
icon: 'account-circle',
|
||||
text: 'Arnaud VERGNET',
|
||||
showChevron: false
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.authorMail),
|
||||
icon: 'email',
|
||||
text: i18n.t('screens.about.authorMail'),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.authorLinkedin),
|
||||
icon: 'linkedin',
|
||||
text: 'Linkedin',
|
||||
showChevron: true
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Data to be displayed in the additional developer card
|
||||
*/
|
||||
additionalDevData = [
|
||||
{
|
||||
onPressCallback: () => console.log('Meme this'),
|
||||
icon: 'account',
|
||||
text: 'Yohan SIMARD',
|
||||
showChevron: false
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.yohanMail),
|
||||
icon: 'email',
|
||||
text: i18n.t('screens.about.authorMail'),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.yohanLinkedin),
|
||||
icon: 'linkedin',
|
||||
text: 'Linkedin',
|
||||
showChevron: true
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Data to be displayed in the technologies card
|
||||
*/
|
||||
technoData = [
|
||||
{
|
||||
onPressCallback: () => openWebLink(links.react),
|
||||
icon: 'react',
|
||||
text: i18n.t('screens.about.reactNative'),
|
||||
showChevron: true
|
||||
},
|
||||
{
|
||||
onPressCallback: () => this.props.navigation.navigate('dependencies'),
|
||||
icon: 'developer-board',
|
||||
text: i18n.t('screens.about.libs'),
|
||||
showChevron: true
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Order of information cards
|
||||
*/
|
||||
dataOrder = [
|
||||
{
|
||||
id: 'app',
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
},
|
||||
{
|
||||
id: 'techno',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Data to be displayed in the author card
|
||||
*/
|
||||
authorData = [
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.meme);
|
||||
},
|
||||
icon: 'account-circle',
|
||||
text: 'Arnaud VERGNET',
|
||||
showChevron: false,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.authorMail);
|
||||
},
|
||||
icon: 'email',
|
||||
text: i18n.t('screens.about.authorMail'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.authorLinkedin);
|
||||
},
|
||||
icon: 'linkedin',
|
||||
text: 'Linkedin',
|
||||
showChevron: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the app icon
|
||||
*
|
||||
* @param props
|
||||
* @return {*}
|
||||
*/
|
||||
getAppIcon(props) {
|
||||
return (
|
||||
/**
|
||||
* Data to be displayed in the additional developer card
|
||||
*/
|
||||
additionalDevData = [
|
||||
{
|
||||
onPressCallback: () => {},
|
||||
icon: 'account',
|
||||
text: 'Yohan SIMARD',
|
||||
showChevron: false,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.yohanMail);
|
||||
},
|
||||
icon: 'email',
|
||||
text: i18n.t('screens.about.authorMail'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.yohanLinkedin);
|
||||
},
|
||||
icon: 'linkedin',
|
||||
text: 'Linkedin',
|
||||
showChevron: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Data to be displayed in the technologies card
|
||||
*/
|
||||
technoData = [
|
||||
{
|
||||
onPressCallback: () => {
|
||||
openWebLink(links.react);
|
||||
},
|
||||
icon: 'react',
|
||||
text: i18n.t('screens.about.reactNative'),
|
||||
showChevron: true,
|
||||
},
|
||||
{
|
||||
onPressCallback: () => {
|
||||
const {navigation} = this.props;
|
||||
navigation.navigate('dependencies');
|
||||
},
|
||||
icon: 'developer-board',
|
||||
text: i18n.t('screens.about.libs'),
|
||||
showChevron: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Order of information cards
|
||||
*/
|
||||
dataOrder = [
|
||||
{
|
||||
id: 'app',
|
||||
},
|
||||
{
|
||||
id: 'team',
|
||||
},
|
||||
{
|
||||
id: 'techno',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the app card showing information and links about the app.
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getAppCard(): React.Node {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title="Campus"
|
||||
subtitle={packageJson.version}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Avatar.Image
|
||||
{...props}
|
||||
source={require('../../../assets/android.icon.png')}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
size={size}
|
||||
source={APP_LOGO}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
);
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<FlatList
|
||||
data={this.appData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the team card showing information and links about the team
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getTeamCard(): React.Node {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.about.team')}
|
||||
left={({size, color}: {size: number, color: string}): React.Node => (
|
||||
<Avatar.Icon size={size} color={color} icon="account-multiple" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.about.author')}</Title>
|
||||
<FlatList
|
||||
data={this.authorData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey="1"
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
<Title>{i18n.t('screens.about.additionalDev')}</Title>
|
||||
<FlatList
|
||||
data={this.additionalDevData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey="2"
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the techno card showing information and links about the technologies used in the app
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getTechnoCard(): React.Node {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.about.technologies')}</Title>
|
||||
<FlatList
|
||||
data={this.technoData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chevron icon
|
||||
*
|
||||
* @param props
|
||||
* @return {*}
|
||||
*/
|
||||
static getChevronIcon({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node {
|
||||
return <List.Icon size={size} color={color} icon="chevron-right" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom list item icon
|
||||
*
|
||||
* @param item The item to show the icon for
|
||||
* @param props
|
||||
* @return {*}
|
||||
*/
|
||||
static getItemIcon(
|
||||
item: ListItemType,
|
||||
{size, color}: {size: number, color: string},
|
||||
): React.Node {
|
||||
return <List.Icon size={size} color={color} icon={item.icon} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a clickable card item to be rendered inside a card.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getCardItem = ({item}: {item: ListItemType}): React.Node => {
|
||||
const getItemIcon = (props: {size: number, color: string}): React.Node =>
|
||||
AboutScreen.getItemIcon(item, props);
|
||||
if (item.showChevron) {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.text}
|
||||
left={getItemIcon}
|
||||
right={AboutScreen.getChevronIcon}
|
||||
onPress={item.onPressCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
title={item.text}
|
||||
left={getItemIcon}
|
||||
onPress={item.onPressCallback}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts a key from the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {string} The extracted key
|
||||
*/
|
||||
keyExtractor(item: ListItem): string {
|
||||
return item.icon;
|
||||
/**
|
||||
* Gets a card, depending on the given item's id
|
||||
*
|
||||
* @param item The item to show
|
||||
* @return {*}
|
||||
*/
|
||||
getMainCard = ({item}: {item: {id: string}}): React.Node => {
|
||||
switch (item.id) {
|
||||
case 'app':
|
||||
return this.getAppCard();
|
||||
case 'team':
|
||||
return this.getTeamCard();
|
||||
case 'techno':
|
||||
return this.getTechnoCard();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the app card showing information and links about the app.
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getAppCard() {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title={"Campus"}
|
||||
subtitle={packageJson.version}
|
||||
left={this.getAppIcon}/>
|
||||
<Card.Content>
|
||||
<FlatList
|
||||
data={this.appData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Extracts a key from the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {string} The extracted key
|
||||
*/
|
||||
keyExtractor = (item: ListItemType): string => item.icon;
|
||||
|
||||
/**
|
||||
* Gets the team card showing information and links about the team
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getTeamCard() {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.about.team')}
|
||||
left={(props) => <Avatar.Icon {...props} icon={'account-multiple'}/>}/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.about.author')}</Title>
|
||||
<FlatList
|
||||
data={this.authorData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey={"1"}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
<Title>{i18n.t('screens.about.additionalDev')}</Title>
|
||||
<FlatList
|
||||
data={this.additionalDevData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey={"2"}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the techno card showing information and links about the technologies used in the app
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getTechnoCard() {
|
||||
return (
|
||||
<Card style={{marginBottom: 10}}>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.about.technologies')}</Title>
|
||||
<FlatList
|
||||
data={this.technoData}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getCardItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chevron icon
|
||||
*
|
||||
* @param props
|
||||
* @return {*}
|
||||
*/
|
||||
getChevronIcon(props) {
|
||||
return (
|
||||
<List.Icon {...props} icon={'chevron-right'}/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom list item icon
|
||||
*
|
||||
* @param item The item to show the icon for
|
||||
* @param props
|
||||
* @return {*}
|
||||
*/
|
||||
getItemIcon(item: ListItem, props) {
|
||||
return (
|
||||
<List.Icon {...props} icon={item.icon}/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a clickable card item to be rendered inside a card.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getCardItem = ({item}: { item: ListItem }) => {
|
||||
const getItemIcon = this.getItemIcon.bind(this, item);
|
||||
if (item.showChevron) {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.text}
|
||||
left={getItemIcon}
|
||||
right={this.getChevronIcon}
|
||||
onPress={item.onPressCallback}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.text}
|
||||
left={getItemIcon}
|
||||
onPress={item.onPressCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a card, depending on the given item's id
|
||||
*
|
||||
* @param item The item to show
|
||||
* @return {*}
|
||||
*/
|
||||
getMainCard = ({item}: { item: { id: string } }) => {
|
||||
switch (item.id) {
|
||||
case 'app':
|
||||
return this.getAppCard();
|
||||
case 'team':
|
||||
return this.getTeamCard();
|
||||
case 'techno':
|
||||
return this.getTechnoCard();
|
||||
}
|
||||
return <View/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsibleFlatList
|
||||
style={{padding: 5}}
|
||||
data={this.dataOrder}
|
||||
renderItem={this.getMainCard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
return (
|
||||
<CollapsibleFlatList
|
||||
style={{padding: 5}}
|
||||
data={this.dataOrder}
|
||||
renderItem={this.getMainCard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AboutScreen);
|
||||
|
|
|
|||
|
|
@ -1,183 +1,211 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
import CustomModal from "../../components/Overrides/CustomModal";
|
||||
import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {Modalize} from "react-native-modalize";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
||||
import {View} from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
List,
|
||||
Subheading,
|
||||
TextInput,
|
||||
Title,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {Modalize} from 'react-native-modalize';
|
||||
import CustomModal from '../../components/Overrides/CustomModal';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||
|
||||
type PreferenceItem = {
|
||||
key: string,
|
||||
default: string,
|
||||
current: string,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme
|
||||
type PreferenceItemType = {
|
||||
key: string,
|
||||
default: string,
|
||||
current: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
modalCurrentDisplayItem: PreferenceItem,
|
||||
currentPreferences: Array<PreferenceItem>,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
modalCurrentDisplayItem: PreferenceItemType,
|
||||
currentPreferences: Array<PreferenceItemType>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Class defining the Debug screen.
|
||||
* This screen allows the user to get and modify information on the app/device.
|
||||
*/
|
||||
class DebugScreen extends React.Component<Props, State> {
|
||||
class DebugScreen extends React.Component<PropsType, StateType> {
|
||||
modalRef: Modalize;
|
||||
|
||||
modalRef: Modalize;
|
||||
modalInputValue: string;
|
||||
modalInputValue: string;
|
||||
|
||||
/**
|
||||
* Copies user preferences to state for easier manipulation
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.modalInputValue = "";
|
||||
let currentPreferences : Array<PreferenceItem> = [];
|
||||
Object.values(AsyncStorageManager.PREFERENCES).map((object: any) => {
|
||||
let newObject: PreferenceItem = {...object};
|
||||
newObject.current = AsyncStorageManager.getString(newObject.key);
|
||||
currentPreferences.push(newObject);
|
||||
});
|
||||
this.state = {
|
||||
modalCurrentDisplayItem: {},
|
||||
currentPreferences: currentPreferences
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the edit modal
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
showEditModal(item: PreferenceItem) {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: item
|
||||
});
|
||||
if (this.modalRef) {
|
||||
this.modalRef.open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the edit modal content
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getModalContent() {
|
||||
return (
|
||||
<View style={{
|
||||
flex: 1,
|
||||
padding: 20
|
||||
}}>
|
||||
<Title>{this.state.modalCurrentDisplayItem.key}</Title>
|
||||
<Subheading>Default: {this.state.modalCurrentDisplayItem.default}</Subheading>
|
||||
<Subheading>Current: {this.state.modalCurrentDisplayItem.current}</Subheading>
|
||||
<TextInput
|
||||
label='New Value'
|
||||
onChangeText={(text) => this.modalInputValue = text}
|
||||
/>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
}}>
|
||||
<Button
|
||||
mode="contained"
|
||||
dark={true}
|
||||
color={this.props.theme.colors.success}
|
||||
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}>
|
||||
Save new value
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
dark={true}
|
||||
color={this.props.theme.colors.danger}
|
||||
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}>
|
||||
Reset to default
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the given key in the preferences array
|
||||
*
|
||||
* @param key THe key to find the index of
|
||||
* @returns {number}
|
||||
*/
|
||||
findIndexOfKey(key: string) {
|
||||
let index = -1;
|
||||
for (let i = 0; i < this.state.currentPreferences.length; i++) {
|
||||
if (this.state.currentPreferences[i].key === key) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new value of the given preference
|
||||
*
|
||||
* @param key The pref key
|
||||
* @param value The pref value
|
||||
*/
|
||||
saveNewPrefs(key: string, value: string) {
|
||||
this.setState((prevState) => {
|
||||
let currentPreferences = [...prevState.currentPreferences];
|
||||
currentPreferences[this.findIndexOfKey(key)].current = value;
|
||||
return {currentPreferences};
|
||||
});
|
||||
AsyncStorageManager.set(key, value);
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when receiving the modal ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onModalRef = (ref: Modalize) => {
|
||||
this.modalRef = ref;
|
||||
}
|
||||
|
||||
renderItem = ({item}: {item: PreferenceItem}) => {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.key}
|
||||
description={'Click to edit'}
|
||||
onPress={() => this.showEditModal(item)}
|
||||
/>
|
||||
);
|
||||
/**
|
||||
* Copies user preferences to state for easier manipulation
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.modalInputValue = '';
|
||||
const currentPreferences: Array<PreferenceItemType> = [];
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => {
|
||||
const newObject: PreferenceItemType = {...object};
|
||||
newObject.current = AsyncStorageManager.getString(newObject.key);
|
||||
currentPreferences.push(newObject);
|
||||
});
|
||||
this.state = {
|
||||
modalCurrentDisplayItem: {},
|
||||
currentPreferences,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<CustomModal onRef={this.onModalRef}>
|
||||
{this.getModalContent()}
|
||||
</CustomModal>
|
||||
{/*$FlowFixMe*/}
|
||||
<CollapsibleFlatList
|
||||
data={this.state.currentPreferences}
|
||||
extraData={this.state.currentPreferences}
|
||||
renderItem={this.renderItem}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
/**
|
||||
* Gets the edit modal content
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getModalContent(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
}}>
|
||||
<Title>{state.modalCurrentDisplayItem.key}</Title>
|
||||
<Subheading>
|
||||
Default: {state.modalCurrentDisplayItem.default}
|
||||
</Subheading>
|
||||
<Subheading>
|
||||
Current: {state.modalCurrentDisplayItem.current}
|
||||
</Subheading>
|
||||
<TextInput
|
||||
label="New Value"
|
||||
onChangeText={(text: string) => {
|
||||
this.modalInputValue = text;
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
}}>
|
||||
<Button
|
||||
mode="contained"
|
||||
dark
|
||||
color={props.theme.colors.success}
|
||||
onPress={() => {
|
||||
this.saveNewPrefs(
|
||||
state.modalCurrentDisplayItem.key,
|
||||
this.modalInputValue,
|
||||
);
|
||||
}}>
|
||||
Save new value
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
dark
|
||||
color={props.theme.colors.danger}
|
||||
onPress={() => {
|
||||
this.saveNewPrefs(
|
||||
state.modalCurrentDisplayItem.key,
|
||||
state.modalCurrentDisplayItem.default,
|
||||
);
|
||||
}}>
|
||||
Reset to default
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getRenderItem = ({item}: {item: PreferenceItemType}): React.Node => {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.key}
|
||||
description="Click to edit"
|
||||
onPress={() => {
|
||||
this.showEditModal(item);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when receiving the modal ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onModalRef = (ref: Modalize) => {
|
||||
this.modalRef = ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the edit modal
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
showEditModal(item: PreferenceItemType) {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: item,
|
||||
});
|
||||
if (this.modalRef) this.modalRef.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the given key in the preferences array
|
||||
*
|
||||
* @param key THe key to find the index of
|
||||
* @returns {number}
|
||||
*/
|
||||
findIndexOfKey(key: string): number {
|
||||
const {currentPreferences} = this.state;
|
||||
let index = -1;
|
||||
for (let i = 0; i < currentPreferences.length; i += 1) {
|
||||
if (currentPreferences[i].key === key) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new value of the given preference
|
||||
*
|
||||
* @param key The pref key
|
||||
* @param value The pref value
|
||||
*/
|
||||
saveNewPrefs(key: string, value: string) {
|
||||
this.setState((prevState: StateType): {
|
||||
currentPreferences: Array<PreferenceItemType>,
|
||||
} => {
|
||||
const currentPreferences = [...prevState.currentPreferences];
|
||||
currentPreferences[this.findIndexOfKey(key)].current = value;
|
||||
return {currentPreferences};
|
||||
});
|
||||
AsyncStorageManager.set(key, value);
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {state} = this;
|
||||
return (
|
||||
<View>
|
||||
<CustomModal onRef={this.onModalRef}>
|
||||
{this.getModalContent()}
|
||||
</CustomModal>
|
||||
{/* $FlowFixMe */}
|
||||
<CollapsibleFlatList
|
||||
data={state.currentPreferences}
|
||||
extraData={state.currentPreferences}
|
||||
renderItem={this.getRenderItem}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(DebugScreen);
|
||||
|
|
|
|||
|
|
@ -4,137 +4,157 @@ import * as React from 'react';
|
|||
import {FlatList, Image, Linking, View} from 'react-native';
|
||||
import {Card, List, Text, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
||||
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||
import AMICALE_LOGO from '../../../assets/amicale.png';
|
||||
|
||||
type Props = {
|
||||
type DatasetItemType = {
|
||||
name: string,
|
||||
email: string,
|
||||
icon: MaterialCommunityIconsGlyphs,
|
||||
};
|
||||
|
||||
type DatasetItem = {
|
||||
name: string,
|
||||
email: string,
|
||||
icon: MaterialCommunityIconsGlyphs,
|
||||
}
|
||||
|
||||
/**
|
||||
* Class defining a planning event information page.
|
||||
*/
|
||||
class AmicaleContactScreen extends React.Component<Props> {
|
||||
class AmicaleContactScreen extends React.Component<null> {
|
||||
// Dataset containing information about contacts
|
||||
CONTACT_DATASET: Array<DatasetItemType>;
|
||||
|
||||
// Dataset containing information about contacts
|
||||
CONTACT_DATASET: Array<DatasetItem>;
|
||||
constructor() {
|
||||
super();
|
||||
this.CONTACT_DATASET = [
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.interSchools'),
|
||||
email: 'inter.ecoles@amicale-insat.fr',
|
||||
icon: 'share-variant',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.culture'),
|
||||
email: 'culture@amicale-insat.fr',
|
||||
icon: 'book',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.animation'),
|
||||
email: 'animation@amicale-insat.fr',
|
||||
icon: 'emoticon',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.clubs'),
|
||||
email: 'clubs@amicale-insat.fr',
|
||||
icon: 'account-group',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.event'),
|
||||
email: 'evenements@amicale-insat.fr',
|
||||
icon: 'calendar-range',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.tech'),
|
||||
email: 'technique@amicale-insat.fr',
|
||||
icon: 'cog',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.communication'),
|
||||
email: 'amicale@amicale-insat.fr',
|
||||
icon: 'comment-account',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.intraSchools'),
|
||||
email: 'intra.ecoles@amicale-insat.fr',
|
||||
icon: 'school',
|
||||
},
|
||||
{
|
||||
name: i18n.t('screens.amicaleAbout.roles.publicRelations'),
|
||||
email: 'rp@amicale-insat.fr',
|
||||
icon: 'account-tie',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.CONTACT_DATASET = [
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.interSchools"),
|
||||
email: "inter.ecoles@amicale-insat.fr",
|
||||
icon: "share-variant"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.culture"),
|
||||
email: "culture@amicale-insat.fr",
|
||||
icon: "book"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.animation"),
|
||||
email: "animation@amicale-insat.fr",
|
||||
icon: "emoticon"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.clubs"),
|
||||
email: "clubs@amicale-insat.fr",
|
||||
icon: "account-group"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.event"),
|
||||
email: "evenements@amicale-insat.fr",
|
||||
icon: "calendar-range"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.tech"),
|
||||
email: "technique@amicale-insat.fr",
|
||||
icon: "cog"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.communication"),
|
||||
email: "amicale@amicale-insat.fr",
|
||||
icon: "comment-account"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.intraSchools"),
|
||||
email: "intra.ecoles@amicale-insat.fr",
|
||||
icon: "school"
|
||||
},
|
||||
{
|
||||
name: i18n.t("screens.amicaleAbout.roles.publicRelations"),
|
||||
email: "rp@amicale-insat.fr",
|
||||
icon: "account-tie"
|
||||
},
|
||||
];
|
||||
}
|
||||
keyExtractor = (item: DatasetItemType): string => item.email;
|
||||
|
||||
keyExtractor = (item: DatasetItem) => item.email;
|
||||
getChevronIcon = ({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="chevron-right" />
|
||||
);
|
||||
|
||||
getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
|
||||
|
||||
renderItem = ({item}: { item: DatasetItem }) => {
|
||||
const onPress = () => Linking.openURL('mailto:' + item.email);
|
||||
return <List.Item
|
||||
title={item.name}
|
||||
description={item.email}
|
||||
left={(props) => <List.Icon {...props} icon={item.icon}/>}
|
||||
right={this.getChevronIcon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
|
||||
const onPress = () => {
|
||||
Linking.openURL(`mailto:${item.email}`);
|
||||
};
|
||||
return (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={item.email}
|
||||
left={({size, color}: {size: number, color: string}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon={item.icon} />
|
||||
)}
|
||||
right={this.getChevronIcon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getScreen = () => {
|
||||
return (
|
||||
<View>
|
||||
<View style={{
|
||||
width: '100%',
|
||||
height: 100,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Image
|
||||
source={require('../../../assets/amicale.png')}
|
||||
style={{flex: 1, resizeMode: "contain"}}
|
||||
resizeMode="contain"/>
|
||||
</View>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t("screens.amicaleAbout.title")}
|
||||
subtitle={i18n.t("screens.amicaleAbout.subtitle")}
|
||||
left={props => <List.Icon {...props} icon={'information'}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Text>{i18n.t("screens.amicaleAbout.message")}</Text>
|
||||
{/*$FlowFixMe*/}
|
||||
<FlatList
|
||||
data={this.CONTACT_DATASET}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsibleFlatList
|
||||
data={[{key: "1"}]}
|
||||
renderItem={this.getScreen}
|
||||
hasTab={true}
|
||||
getScreen = (): React.Node => {
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 100,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Image
|
||||
source={AMICALE_LOGO}
|
||||
style={{flex: 1, resizeMode: 'contain'}}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.amicaleAbout.title')}
|
||||
subtitle={i18n.t('screens.amicaleAbout.subtitle')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="information" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Text>{i18n.t('screens.amicaleAbout.message')}</Text>
|
||||
<FlatList
|
||||
data={this.CONTACT_DATASET}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
return (
|
||||
<CollapsibleFlatList
|
||||
data={[{key: '1'}]}
|
||||
renderItem={this.getScreen}
|
||||
hasTab
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AmicaleContactScreen);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen
|
|||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||
import type {ClubCategoryType, ClubType} from './ClubListScreen';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import {ERROR_TYPE} from '../../../utils/WebData';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import type {ApiGenericDataType} from '../../../utils/WebData';
|
||||
|
|
@ -32,7 +32,7 @@ type PropsType = {
|
|||
},
|
||||
...
|
||||
},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {DeviceType} from './EquipmentListScreen';
|
||||
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
|
|
@ -23,7 +23,7 @@ type PropsType = {
|
|||
dates: [string, string],
|
||||
},
|
||||
},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class EquipmentConfirmScreen extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import * as Animatable from 'react-native-animatable';
|
|||
import i18n from 'i18n-js';
|
||||
import {CalendarList} from 'react-native-calendars';
|
||||
import type {DeviceType} from './EquipmentListScreen';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
||||
import {
|
||||
|
|
@ -36,7 +36,7 @@ type PropsType = {
|
|||
item?: DeviceType,
|
||||
},
|
||||
},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
export type MarkedDatesObjectType = {
|
||||
|
|
|
|||
|
|
@ -1,417 +1,446 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native";
|
||||
import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
|
||||
import ConnectionManager from "../../managers/ConnectionManager";
|
||||
import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
HelperText,
|
||||
TextInput,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import ErrorDialog from "../../components/Dialogs/ErrorDialog";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: { params: { nextScreen: string } },
|
||||
theme: CustomTheme
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {nextScreen: string}},
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type State = {
|
||||
email: string,
|
||||
password: string,
|
||||
isEmailValidated: boolean,
|
||||
isPasswordValidated: boolean,
|
||||
loading: boolean,
|
||||
dialogVisible: boolean,
|
||||
dialogError: number,
|
||||
mascotDialogVisible: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
email: string,
|
||||
password: string,
|
||||
isEmailValidated: boolean,
|
||||
isPasswordValidated: boolean,
|
||||
loading: boolean,
|
||||
dialogVisible: boolean,
|
||||
dialogError: number,
|
||||
mascotDialogVisible: boolean,
|
||||
};
|
||||
|
||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||
|
||||
const RESET_PASSWORD_PATH = "https://www.amicale-insat.fr/password/reset";
|
||||
const RESET_PASSWORD_PATH = 'https://www.amicale-insat.fr/password/reset';
|
||||
|
||||
const emailRegex = /^.+@.+\..+$/;
|
||||
|
||||
class LoginScreen extends React.Component<Props, State> {
|
||||
|
||||
state = {
|
||||
email: '',
|
||||
password: '',
|
||||
isEmailValidated: false,
|
||||
isPasswordValidated: false,
|
||||
loading: false,
|
||||
dialogVisible: false,
|
||||
dialogError: 0,
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.loginShowBanner.key),
|
||||
};
|
||||
|
||||
onEmailChange: (value: string) => null;
|
||||
onPasswordChange: (value: string) => null;
|
||||
passwordInputRef: { current: null | TextInput };
|
||||
|
||||
nextScreen: string | null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.passwordInputRef = React.createRef();
|
||||
this.onEmailChange = this.onInputChange.bind(this, true);
|
||||
this.onPasswordChange = this.onInputChange.bind(this, false);
|
||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
||||
}
|
||||
|
||||
onScreenFocus = () => {
|
||||
this.handleNavigationParams();
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
|
||||
*/
|
||||
handleNavigationParams() {
|
||||
if (this.props.route.params != null) {
|
||||
if (this.props.route.params.nextScreen != null)
|
||||
this.nextScreen = this.props.route.params.nextScreen;
|
||||
else
|
||||
this.nextScreen = null;
|
||||
}
|
||||
}
|
||||
|
||||
hideMascotDialog = () => {
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.loginShowBanner.key, false);
|
||||
this.setState({mascotDialogVisible: false})
|
||||
};
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({mascotDialogVisible: true})
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an error dialog with the corresponding login error
|
||||
*
|
||||
* @param error The error given by the login request
|
||||
*/
|
||||
showErrorDialog = (error: number) =>
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogError: error,
|
||||
});
|
||||
|
||||
hideErrorDialog = () => this.setState({dialogVisible: false});
|
||||
|
||||
/**
|
||||
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
|
||||
* Saves in user preferences to not show the login banner again.
|
||||
*/
|
||||
handleSuccess = () => {
|
||||
// Do not show the home login banner again
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.homeShowBanner.key, false);
|
||||
if (this.nextScreen == null)
|
||||
this.props.navigation.goBack();
|
||||
else
|
||||
this.props.navigation.replace(this.nextScreen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the Amicale website screen with the reset password link as navigation parameters
|
||||
*/
|
||||
onResetPasswordClick = () => this.props.navigation.navigate("website", {
|
||||
host: AvailableWebsites.websites.AMICALE,
|
||||
path: RESET_PASSWORD_PATH,
|
||||
title: i18n.t('screens.websites.amicale')
|
||||
});
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his email is ready to be validated
|
||||
*/
|
||||
validateEmail = () => this.setState({isEmailValidated: true});
|
||||
|
||||
/**
|
||||
* Checks if the entered email is valid (matches the regex)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmailValid() {
|
||||
return emailRegex.test(this.state.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his email is invalid.
|
||||
* We should only show this if his email is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowEmailError() {
|
||||
return this.state.isEmailValidated && !this.isEmailValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his password is ready to be validated
|
||||
*/
|
||||
validatePassword = () => this.setState({isPasswordValidated: true});
|
||||
|
||||
/**
|
||||
* Checks if the user has entered a password
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPasswordValid() {
|
||||
return this.state.password !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his password is invalid.
|
||||
* We should only show this if his password is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowPasswordError() {
|
||||
return this.state.isPasswordValidated && !this.isPasswordValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldEnableLogin() {
|
||||
return this.isEmailValid() && this.isPasswordValid() && !this.state.loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user input changes in the email or password field.
|
||||
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
|
||||
*
|
||||
* @param isEmail True if the field is the email field
|
||||
* @param value The new field value
|
||||
*/
|
||||
onInputChange(isEmail: boolean, value: string) {
|
||||
if (isEmail) {
|
||||
this.setState({
|
||||
email: value,
|
||||
isEmailValidated: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
password: value,
|
||||
isPasswordValidated: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the password field when the email field is done
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
onEmailSubmit = () => {
|
||||
if (this.passwordInputRef.current != null)
|
||||
this.passwordInputRef.current.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks on login or finishes to type his password.
|
||||
*
|
||||
* Checks if we should allow the user to login,
|
||||
* then makes the login request and enters a loading state until the request finishes
|
||||
*
|
||||
*/
|
||||
onSubmit = () => {
|
||||
if (this.shouldEnableLogin()) {
|
||||
this.setState({loading: true});
|
||||
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.showErrorDialog)
|
||||
.finally(() => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the form input
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getFormInput() {
|
||||
return (
|
||||
<View>
|
||||
<TextInput
|
||||
label={i18n.t("screens.login.email")}
|
||||
mode='outlined'
|
||||
value={this.state.email}
|
||||
onChangeText={this.onEmailChange}
|
||||
onBlur={this.validateEmail}
|
||||
onSubmitEditing={this.onEmailSubmit}
|
||||
error={this.shouldShowEmailError()}
|
||||
textContentType={'emailAddress'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'email'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'email-address'}
|
||||
returnKeyType={'next'}
|
||||
secureTextEntry={false}
|
||||
/>
|
||||
<HelperText
|
||||
type="error"
|
||||
visible={this.shouldShowEmailError()}
|
||||
>
|
||||
{i18n.t("screens.login.emailError")}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
ref={this.passwordInputRef}
|
||||
label={i18n.t("screens.login.password")}
|
||||
mode='outlined'
|
||||
value={this.state.password}
|
||||
onChangeText={this.onPasswordChange}
|
||||
onBlur={this.validatePassword}
|
||||
onSubmitEditing={this.onSubmit}
|
||||
error={this.shouldShowPasswordError()}
|
||||
textContentType={'password'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'password'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'default'}
|
||||
returnKeyType={'done'}
|
||||
secureTextEntry={true}
|
||||
/>
|
||||
<HelperText
|
||||
type="error"
|
||||
visible={this.shouldShowPasswordError()}
|
||||
>
|
||||
{i18n.t("screens.login.passwordError")}
|
||||
</HelperText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the card containing the input form
|
||||
* @returns {*}
|
||||
*/
|
||||
getMainCard() {
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t("screens.login.title")}
|
||||
titleStyle={{color: "#fff"}}
|
||||
subtitle={i18n.t("screens.login.subtitle")}
|
||||
subtitleStyle={{color: "#fff"}}
|
||||
left={(props) => <Image
|
||||
{...props}
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
width: props.size,
|
||||
height: props.size,
|
||||
}}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
{this.getFormInput()}
|
||||
<Card.Actions style={{flexWrap: "wrap"}}>
|
||||
<Button
|
||||
icon="lock-question"
|
||||
mode="contained"
|
||||
onPress={this.onResetPasswordClick}
|
||||
color={this.props.theme.colors.warning}
|
||||
style={{marginRight: 'auto', marginBottom: 20}}>
|
||||
{i18n.t("screens.login.resetPassword")}
|
||||
</Button>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
disabled={!this.shouldEnableLogin()}
|
||||
loading={this.state.loading}
|
||||
onPress={this.onSubmit}
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t("screens.login.title")}
|
||||
</Button>
|
||||
|
||||
</Card.Actions>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="help-circle"
|
||||
mode="contained"
|
||||
onPress={this.showMascotDialog}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
{i18n.t("screens.login.mascotDialog.title")}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LinearGradient
|
||||
style={{
|
||||
height: "100%"
|
||||
}}
|
||||
colors={['#9e0d18', '#530209']}
|
||||
start={{x: 0, y: 0.1}}
|
||||
end={{x: 0.1, y: 1}}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={"height"}
|
||||
contentContainerStyle={styles.container}
|
||||
style={styles.container}
|
||||
enabled
|
||||
keyboardVerticalOffset={100}
|
||||
>
|
||||
<CollapsibleScrollView>
|
||||
<View style={{height: "100%"}}>
|
||||
{this.getMainCard()}
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
title={i18n.t("screens.login.mascotDialog.title")}
|
||||
message={i18n.t("screens.login.mascotDialog.message")}
|
||||
icon={"help"}
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t("screens.login.mascotDialog.button"),
|
||||
icon: "check",
|
||||
onPress: this.hideMascotDialog,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={this.state.dialogVisible}
|
||||
onDismiss={this.hideErrorDialog}
|
||||
errorCode={this.state.dialogError}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
card: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
header: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48
|
||||
},
|
||||
textInput: {},
|
||||
btnContainer: {
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
}
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
card: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
header: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48,
|
||||
},
|
||||
textInput: {},
|
||||
btnContainer: {
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
class LoginScreen extends React.Component<PropsType, StateType> {
|
||||
onEmailChange: (value: string) => void;
|
||||
|
||||
onPasswordChange: (value: string) => void;
|
||||
|
||||
passwordInputRef: {current: null | TextInput};
|
||||
|
||||
nextScreen: string | null;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.passwordInputRef = React.createRef();
|
||||
this.onEmailChange = (value: string) => {
|
||||
this.onInputChange(true, value);
|
||||
};
|
||||
this.onPasswordChange = (value: string) => {
|
||||
this.onInputChange(false, value);
|
||||
};
|
||||
props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.state = {
|
||||
email: '',
|
||||
password: '',
|
||||
isEmailValidated: false,
|
||||
isPasswordValidated: false,
|
||||
loading: false,
|
||||
dialogVisible: false,
|
||||
dialogError: 0,
|
||||
mascotDialogVisible: AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
onScreenFocus = () => {
|
||||
this.handleNavigationParams();
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the Amicale website screen with the reset password link as navigation parameters
|
||||
*/
|
||||
onResetPasswordClick = () => {
|
||||
const {navigation} = this.props;
|
||||
navigation.navigate('website', {
|
||||
host: AvailableWebsites.websites.AMICALE,
|
||||
path: RESET_PASSWORD_PATH,
|
||||
title: i18n.t('screens.websites.amicale'),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the user input changes in the email or password field.
|
||||
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
|
||||
*
|
||||
* @param isEmail True if the field is the email field
|
||||
* @param value The new field value
|
||||
*/
|
||||
onInputChange(isEmail: boolean, value: string) {
|
||||
if (isEmail) {
|
||||
this.setState({
|
||||
email: value,
|
||||
isEmailValidated: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
password: value,
|
||||
isPasswordValidated: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the password field when the email field is done
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
onEmailSubmit = () => {
|
||||
if (this.passwordInputRef.current != null)
|
||||
this.passwordInputRef.current.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the user clicks on login or finishes to type his password.
|
||||
*
|
||||
* Checks if we should allow the user to login,
|
||||
* then makes the login request and enters a loading state until the request finishes
|
||||
*
|
||||
*/
|
||||
onSubmit = () => {
|
||||
const {email, password} = this.state;
|
||||
if (this.shouldEnableLogin()) {
|
||||
this.setState({loading: true});
|
||||
ConnectionManager.getInstance()
|
||||
.connect(email, password)
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.showErrorDialog)
|
||||
.finally(() => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the form input
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getFormInput(): React.Node {
|
||||
const {email, password} = this.state;
|
||||
return (
|
||||
<View>
|
||||
<TextInput
|
||||
label={i18n.t('screens.login.email')}
|
||||
mode="outlined"
|
||||
value={email}
|
||||
onChangeText={this.onEmailChange}
|
||||
onBlur={this.validateEmail}
|
||||
onSubmitEditing={this.onEmailSubmit}
|
||||
error={this.shouldShowEmailError()}
|
||||
textContentType="emailAddress"
|
||||
autoCapitalize="none"
|
||||
autoCompleteType="email"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
returnKeyType="next"
|
||||
secureTextEntry={false}
|
||||
/>
|
||||
<HelperText type="error" visible={this.shouldShowEmailError()}>
|
||||
{i18n.t('screens.login.emailError')}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
ref={this.passwordInputRef}
|
||||
label={i18n.t('screens.login.password')}
|
||||
mode="outlined"
|
||||
value={password}
|
||||
onChangeText={this.onPasswordChange}
|
||||
onBlur={this.validatePassword}
|
||||
onSubmitEditing={this.onSubmit}
|
||||
error={this.shouldShowPasswordError()}
|
||||
textContentType="password"
|
||||
autoCapitalize="none"
|
||||
autoCompleteType="password"
|
||||
autoCorrect={false}
|
||||
keyboardType="default"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
/>
|
||||
<HelperText type="error" visible={this.shouldShowPasswordError()}>
|
||||
{i18n.t('screens.login.passwordError')}
|
||||
</HelperText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the card containing the input form
|
||||
* @returns {*}
|
||||
*/
|
||||
getMainCard(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.login.title')}
|
||||
titleStyle={{color: '#fff'}}
|
||||
subtitle={i18n.t('screens.login.subtitle')}
|
||||
subtitleStyle={{color: '#fff'}}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Image
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
{this.getFormInput()}
|
||||
<Card.Actions style={{flexWrap: 'wrap'}}>
|
||||
<Button
|
||||
icon="lock-question"
|
||||
mode="contained"
|
||||
onPress={this.onResetPasswordClick}
|
||||
color={props.theme.colors.warning}
|
||||
style={{marginRight: 'auto', marginBottom: 20}}>
|
||||
{i18n.t('screens.login.resetPassword')}
|
||||
</Button>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
disabled={!this.shouldEnableLogin()}
|
||||
loading={state.loading}
|
||||
onPress={this.onSubmit}
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('screens.login.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="help-circle"
|
||||
mode="contained"
|
||||
onPress={this.showMascotDialog}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
{i18n.t('screens.login.mascotDialog.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his email is ready to be validated
|
||||
*/
|
||||
validateEmail = () => {
|
||||
this.setState({isEmailValidated: true});
|
||||
};
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his password is ready to be validated
|
||||
*/
|
||||
validatePassword = () => {
|
||||
this.setState({isPasswordValidated: true});
|
||||
};
|
||||
|
||||
hideMascotDialog = () => {
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
|
||||
false,
|
||||
);
|
||||
this.setState({mascotDialogVisible: false});
|
||||
};
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({mascotDialogVisible: true});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an error dialog with the corresponding login error
|
||||
*
|
||||
* @param error The error given by the login request
|
||||
*/
|
||||
showErrorDialog = (error: number) => {
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogError: error,
|
||||
});
|
||||
};
|
||||
|
||||
hideErrorDialog = () => {
|
||||
this.setState({dialogVisible: false});
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
|
||||
* Saves in user preferences to not show the login banner again.
|
||||
*/
|
||||
handleSuccess = () => {
|
||||
const {navigation} = this.props;
|
||||
// Do not show the home login banner again
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.homeShowBanner.key,
|
||||
false,
|
||||
);
|
||||
if (this.nextScreen == null) navigation.goBack();
|
||||
else navigation.replace(this.nextScreen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
|
||||
*/
|
||||
handleNavigationParams() {
|
||||
const {route} = this.props;
|
||||
if (route.params != null) {
|
||||
if (route.params.nextScreen != null)
|
||||
this.nextScreen = route.params.nextScreen;
|
||||
else this.nextScreen = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entered email is valid (matches the regex)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmailValid(): boolean {
|
||||
const {email} = this.state;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his email is invalid.
|
||||
* We should only show this if his email is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowEmailError(): boolean {
|
||||
const {isEmailValidated} = this.state;
|
||||
return isEmailValidated && !this.isEmailValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has entered a password
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPasswordValid(): boolean {
|
||||
const {password} = this.state;
|
||||
return password !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his password is invalid.
|
||||
* We should only show this if his password is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowPasswordError(): boolean {
|
||||
const {isPasswordValidated} = this.state;
|
||||
return isPasswordValidated && !this.isPasswordValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldEnableLogin(): boolean {
|
||||
const {loading} = this.state;
|
||||
return this.isEmailValid() && this.isPasswordValid() && !loading;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {mascotDialogVisible, dialogVisible, dialogError} = this.state;
|
||||
return (
|
||||
<LinearGradient
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
colors={['#9e0d18', '#530209']}
|
||||
start={{x: 0, y: 0.1}}
|
||||
end={{x: 0.1, y: 1}}>
|
||||
<KeyboardAvoidingView
|
||||
behavior="height"
|
||||
contentContainerStyle={styles.container}
|
||||
style={styles.container}
|
||||
enabled
|
||||
keyboardVerticalOffset={100}>
|
||||
<CollapsibleScrollView>
|
||||
<View style={{height: '100%'}}>{this.getMainCard()}</View>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.login.mascotDialog.title')}
|
||||
message={i18n.t('screens.login.mascotDialog.message')}
|
||||
icon="help"
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t('screens.login.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideErrorDialog}
|
||||
errorCode={dialogError}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(LoginScreen);
|
||||
|
|
|
|||
|
|
@ -1,432 +1,467 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {FlatList, StyleSheet, View} from "react-native";
|
||||
import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
|
||||
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
|
||||
import {FlatList, StyleSheet, View} from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
List,
|
||||
Paragraph,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import LogoutDialog from "../../components/Amicale/LogoutDialog";
|
||||
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
|
||||
import type {cardList} from "../../components/Lists/CardList/CardList";
|
||||
import CardList from "../../components/Lists/CardList/CardList";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
||||
import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
||||
import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
|
||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import CardList from '../../components/Lists/CardList/CardList';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||
import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
|
||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||
import type {ServiceItemType} from '../../managers/ServicesManager';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type State = {
|
||||
dialogVisible: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
dialogVisible: boolean,
|
||||
};
|
||||
|
||||
type ProfileData = {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
email: string,
|
||||
birthday: string,
|
||||
phone: string,
|
||||
branch: string,
|
||||
link: string,
|
||||
validity: boolean,
|
||||
clubs: Array<Club>,
|
||||
}
|
||||
type Club = {
|
||||
id: number,
|
||||
name: string,
|
||||
is_manager: boolean,
|
||||
}
|
||||
type ClubType = {
|
||||
id: number,
|
||||
name: string,
|
||||
is_manager: boolean,
|
||||
};
|
||||
|
||||
class ProfileScreen extends React.Component<Props, State> {
|
||||
|
||||
state = {
|
||||
dialogVisible: false,
|
||||
};
|
||||
|
||||
data: ProfileData;
|
||||
|
||||
flatListData: Array<{ id: string }>;
|
||||
amicaleDataset: cardList;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.flatListData = [
|
||||
{id: '0'},
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
]
|
||||
const services = new ServicesManager(props.navigation);
|
||||
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
});
|
||||
}
|
||||
|
||||
showDisconnectDialog = () => this.setState({dialogVisible: true});
|
||||
|
||||
hideDisconnectDialog = () => this.setState({dialogVisible: false});
|
||||
|
||||
/**
|
||||
* Gets the logout header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = () => <MaterialHeaderButtons>
|
||||
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
|
||||
</MaterialHeaderButtons>;
|
||||
|
||||
/**
|
||||
* Gets the main screen component with the fetched data
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @returns {*}
|
||||
*/
|
||||
getScreen = (data: Array<{ [key: string]: any } | null>) => {
|
||||
if (data[0] != null) {
|
||||
this.data = data[0];
|
||||
}
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<CollapsibleFlatList
|
||||
renderItem={this.getRenderItem}
|
||||
data={this.flatListData}
|
||||
/>
|
||||
<LogoutDialog
|
||||
{...this.props}
|
||||
visible={this.state.dialogVisible}
|
||||
onDismiss={this.hideDisconnectDialog}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
};
|
||||
|
||||
getRenderItem = ({item}: { item: { id: string } }) => {
|
||||
switch (item.id) {
|
||||
case '0':
|
||||
return this.getWelcomeCard();
|
||||
case '1':
|
||||
return this.getPersonalCard();
|
||||
case '2':
|
||||
return this.getClubCard();
|
||||
default:
|
||||
return this.getMembershipCar();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of services available with the Amicale account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getServicesList() {
|
||||
return (
|
||||
<CardList
|
||||
dataset={this.amicaleDataset}
|
||||
isHorizontal={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card welcoming the user to his account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getWelcomeCard() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
|
||||
left={() =>
|
||||
<Mascot
|
||||
style={{
|
||||
width: 60
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated={true}
|
||||
entryAnimation={{
|
||||
animation: "bounceIn",
|
||||
duration: 1000
|
||||
}}
|
||||
/>}
|
||||
titleStyle={{marginLeft: 10}}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider/>
|
||||
<Paragraph>
|
||||
{i18n.t("screens.profile.welcomeDescription")}
|
||||
</Paragraph>
|
||||
{this.getServicesList()}
|
||||
<Paragraph>
|
||||
{i18n.t("screens.profile.welcomeFeedback")}
|
||||
</Paragraph>
|
||||
<Divider/>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="bug"
|
||||
mode="contained"
|
||||
onPress={() => this.props.navigation.navigate('feedback')}
|
||||
style={styles.editButton}>
|
||||
{i18n.t("screens.feedback.homeButtonTitle")}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given field is available
|
||||
*
|
||||
* @param field The field to check
|
||||
* @return {boolean}
|
||||
*/
|
||||
isFieldAvailable(field: ?string) {
|
||||
return field !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given field value.
|
||||
* If the field does not have a value, returns a placeholder text
|
||||
*
|
||||
* @param field The field to get the value from
|
||||
* @return {*}
|
||||
*/
|
||||
getFieldValue(field: ?string) {
|
||||
return this.isFieldAvailable(field)
|
||||
? field
|
||||
: i18n.t("screens.profile.noData");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list item showing personal information
|
||||
*
|
||||
* @param field The field to display
|
||||
* @param icon The icon to use
|
||||
* @return {*}
|
||||
*/
|
||||
getPersonalListItem(field: ?string, icon: string) {
|
||||
let title = this.isFieldAvailable(field) ? this.getFieldValue(field) : ':(';
|
||||
let subtitle = this.isFieldAvailable(field) ? '' : this.getFieldValue(field);
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={props => <List.Icon
|
||||
{...props}
|
||||
icon={icon}
|
||||
color={this.isFieldAvailable(field) ? undefined : this.props.theme.colors.textDisabled}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card containing user personal information
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getPersonalCard() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={this.data.first_name + ' ' + this.data.last_name}
|
||||
subtitle={this.data.email}
|
||||
left={(props) => <Avatar.Icon
|
||||
{...props}
|
||||
icon="account"
|
||||
color={this.props.theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider/>
|
||||
<List.Section>
|
||||
<List.Subheader>{i18n.t("screens.profile.personalInformation")}</List.Subheader>
|
||||
{this.getPersonalListItem(this.data.birthday, "cake-variant")}
|
||||
{this.getPersonalListItem(this.data.phone, "phone")}
|
||||
{this.getPersonalListItem(this.data.email, "email")}
|
||||
{this.getPersonalListItem(this.data.branch, "school")}
|
||||
</List.Section>
|
||||
<Divider/>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="account-edit"
|
||||
mode="contained"
|
||||
onPress={() => this.props.navigation.navigate("website", {
|
||||
host: AvailableWebsites.websites.AMICALE,
|
||||
path: this.data.link,
|
||||
title: i18n.t('screens.websites.amicale')
|
||||
})}
|
||||
style={styles.editButton}>
|
||||
{i18n.t("screens.profile.editInformation")}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cars containing clubs the user is part of
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getClubCard() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t("screens.profile.clubs")}
|
||||
subtitle={i18n.t("screens.profile.clubsSubtitle")}
|
||||
left={(props) => <Avatar.Icon
|
||||
{...props}
|
||||
icon="account-group"
|
||||
color={this.props.theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider/>
|
||||
{this.getClubList(this.data.clubs)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card showing if the user has payed his membership
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getMembershipCar() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t("screens.profile.membership")}
|
||||
subtitle={i18n.t("screens.profile.membershipSubtitle")}
|
||||
left={(props) => <Avatar.Icon
|
||||
{...props}
|
||||
icon="credit-card"
|
||||
color={this.props.theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<List.Section>
|
||||
{this.getMembershipItem(this.data.validity)}
|
||||
</List.Section>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item showing if the user has payed his membership
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getMembershipItem(state: boolean) {
|
||||
return (
|
||||
<List.Item
|
||||
title={state ? i18n.t("screens.profile.membershipPayed") : i18n.t("screens.profile.membershipNotPayed")}
|
||||
left={props => <List.Icon
|
||||
{...props}
|
||||
color={state ? this.props.theme.colors.success : this.props.theme.colors.danger}
|
||||
icon={state ? 'check' : 'close'}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the club details screen for the club of given ID
|
||||
* @param id The club's id to open
|
||||
*/
|
||||
openClubDetailsScreen(id: number) {
|
||||
this.props.navigation.navigate("club-information", {clubId: id});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list item for the club list
|
||||
*
|
||||
* @param item The club to render
|
||||
* @return {*}
|
||||
*/
|
||||
clubListItem = ({item}: { item: Club }) => {
|
||||
const onPress = () => this.openClubDetailsScreen(item.id);
|
||||
let description = i18n.t("screens.profile.isMember");
|
||||
let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
|
||||
if (item.is_manager) {
|
||||
description = i18n.t("screens.profile.isManager");
|
||||
icon = (props) => <List.Icon {...props} icon="star" color={this.props.theme.colors.primary}/>;
|
||||
}
|
||||
return <List.Item
|
||||
title={item.name}
|
||||
description={description}
|
||||
left={icon}
|
||||
onPress={onPress}
|
||||
/>;
|
||||
};
|
||||
|
||||
clubKeyExtractor = (item: Club) => item.name;
|
||||
|
||||
sortClubList = (a: Club, b: Club) => a.is_manager ? -1 : 1;
|
||||
|
||||
/**
|
||||
* Renders the list of clubs the user is part of
|
||||
*
|
||||
* @param list The club list
|
||||
* @return {*}
|
||||
*/
|
||||
getClubList(list: Array<Club>) {
|
||||
list.sort(this.sortClubList);
|
||||
return (
|
||||
//$FlowFixMe
|
||||
<FlatList
|
||||
renderItem={this.clubListItem}
|
||||
keyExtractor={this.clubKeyExtractor}
|
||||
data={list}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AuthenticatedScreen
|
||||
{...this.props}
|
||||
requests={[
|
||||
{
|
||||
link: 'user/profile',
|
||||
params: {},
|
||||
mandatory: true,
|
||||
}
|
||||
]}
|
||||
renderFunction={this.getScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
type ProfileDataType = {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
email: string,
|
||||
birthday: string,
|
||||
phone: string,
|
||||
branch: string,
|
||||
link: string,
|
||||
validity: boolean,
|
||||
clubs: Array<ClubType>,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto'
|
||||
}
|
||||
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||
data: ProfileDataType;
|
||||
|
||||
flatListData: Array<{id: string}>;
|
||||
|
||||
amicaleDataset: Array<ServiceItemType>;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.flatListData = [{id: '0'}, {id: '1'}, {id: '2'}, {id: '3'}];
|
||||
const services = new ServicesManager(props.navigation);
|
||||
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
|
||||
this.state = {
|
||||
dialogVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {navigation} = this.props;
|
||||
navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logout header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = (): React.Node => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="logout"
|
||||
iconName="logout"
|
||||
onPress={this.showDisconnectDialog}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the main screen component with the fetched data
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @returns {*}
|
||||
*/
|
||||
getScreen = (data: Array<ProfileDataType | null>): React.Node => {
|
||||
const {dialogVisible} = this.state;
|
||||
const {navigation} = this.props;
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
if (data[0] != null) this.data = data[0];
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<CollapsibleFlatList
|
||||
renderItem={this.getRenderItem}
|
||||
data={this.flatListData}
|
||||
/>
|
||||
<LogoutDialog
|
||||
navigation={navigation}
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideDisconnectDialog}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
getRenderItem = ({item}: {item: {id: string}}): React.Node => {
|
||||
switch (item.id) {
|
||||
case '0':
|
||||
return this.getWelcomeCard();
|
||||
case '1':
|
||||
return this.getPersonalCard();
|
||||
case '2':
|
||||
return this.getClubCard();
|
||||
default:
|
||||
return this.getMembershipCar();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of services available with the Amicale account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getServicesList(): React.Node {
|
||||
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card welcoming the user to his account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getWelcomeCard(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.welcomeTitle', {
|
||||
name: this.data.first_name,
|
||||
})}
|
||||
left={(): React.Node => (
|
||||
<Mascot
|
||||
style={{
|
||||
width: 60,
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
titleStyle={{marginLeft: 10}}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
|
||||
{this.getServicesList()}
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="bug"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate('feedback');
|
||||
}}
|
||||
style={styles.editButton}>
|
||||
{i18n.t('screens.feedback.homeButtonTitle')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given field value.
|
||||
* If the field does not have a value, returns a placeholder text
|
||||
*
|
||||
* @param field The field to get the value from
|
||||
* @return {*}
|
||||
*/
|
||||
static getFieldValue(field: ?string): string {
|
||||
return field != null ? field : i18n.t('screens.profile.noData');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list item showing personal information
|
||||
*
|
||||
* @param field The field to display
|
||||
* @param icon The icon to use
|
||||
* @return {*}
|
||||
*/
|
||||
getPersonalListItem(field: ?string, icon: string): React.Node {
|
||||
const {theme} = this.props;
|
||||
const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
|
||||
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<List.Icon
|
||||
size={size}
|
||||
icon={icon}
|
||||
color={field != null ? null : theme.colors.textDisabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card containing user personal information
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getPersonalCard(): React.Node {
|
||||
const {theme, navigation} = this.props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={`${this.data.first_name} ${this.data.last_name}`}
|
||||
subtitle={this.data.email}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Avatar.Icon
|
||||
size={size}
|
||||
icon="account"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<List.Section>
|
||||
<List.Subheader>
|
||||
{i18n.t('screens.profile.personalInformation')}
|
||||
</List.Subheader>
|
||||
{this.getPersonalListItem(this.data.birthday, 'cake-variant')}
|
||||
{this.getPersonalListItem(this.data.phone, 'phone')}
|
||||
{this.getPersonalListItem(this.data.email, 'email')}
|
||||
{this.getPersonalListItem(this.data.branch, 'school')}
|
||||
</List.Section>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="account-edit"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate('website', {
|
||||
host: AvailableWebsites.websites.AMICALE,
|
||||
path: this.data.link,
|
||||
title: i18n.t('screens.websites.amicale'),
|
||||
});
|
||||
}}
|
||||
style={styles.editButton}>
|
||||
{i18n.t('screens.profile.editInformation')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cars containing clubs the user is part of
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getClubCard(): React.Node {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.clubs')}
|
||||
subtitle={i18n.t('screens.profile.clubsSubtitle')}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Avatar.Icon
|
||||
size={size}
|
||||
icon="account-group"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
{this.getClubList(this.data.clubs)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a card showing if the user has payed his membership
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getMembershipCar(): React.Node {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.membership')}
|
||||
subtitle={i18n.t('screens.profile.membershipSubtitle')}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Avatar.Icon
|
||||
size={size}
|
||||
icon="credit-card"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<List.Section>
|
||||
{this.getMembershipItem(this.data.validity)}
|
||||
</List.Section>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item showing if the user has payed his membership
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getMembershipItem(state: boolean): React.Node {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<List.Item
|
||||
title={
|
||||
state
|
||||
? i18n.t('screens.profile.membershipPayed')
|
||||
: i18n.t('screens.profile.membershipNotPayed')
|
||||
}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<List.Icon
|
||||
size={size}
|
||||
color={state ? theme.colors.success : theme.colors.danger}
|
||||
icon={state ? 'check' : 'close'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list item for the club list
|
||||
*
|
||||
* @param item The club to render
|
||||
* @return {*}
|
||||
*/
|
||||
getClubListItem = ({item}: {item: ClubType}): React.Node => {
|
||||
const {theme} = this.props;
|
||||
const onPress = () => {
|
||||
this.openClubDetailsScreen(item.id);
|
||||
};
|
||||
let description = i18n.t('screens.profile.isMember');
|
||||
let icon = ({size, color}: {size: number, color: string}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="chevron-right" />
|
||||
);
|
||||
if (item.is_manager) {
|
||||
description = i18n.t('screens.profile.isManager');
|
||||
icon = ({size}: {size: number}): React.Node => (
|
||||
<List.Icon size={size} icon="star" color={theme.colors.primary} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={description}
|
||||
left={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the list of clubs the user is part of
|
||||
*
|
||||
* @param list The club list
|
||||
* @return {*}
|
||||
*/
|
||||
getClubList(list: Array<ClubType>): React.Node {
|
||||
list.sort(this.sortClubList);
|
||||
return (
|
||||
<FlatList
|
||||
renderItem={this.getClubListItem}
|
||||
keyExtractor={this.clubKeyExtractor}
|
||||
data={list}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
clubKeyExtractor = (item: ClubType): string => item.name;
|
||||
|
||||
sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
|
||||
|
||||
showDisconnectDialog = () => {
|
||||
this.setState({dialogVisible: true});
|
||||
};
|
||||
|
||||
hideDisconnectDialog = () => {
|
||||
this.setState({dialogVisible: false});
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the club details screen for the club of given ID
|
||||
* @param id The club's id to open
|
||||
*/
|
||||
openClubDetailsScreen(id: number) {
|
||||
const {navigation} = this.props;
|
||||
navigation.navigate('club-information', {clubId: id});
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<AuthenticatedScreen
|
||||
navigation={navigation}
|
||||
requests={[
|
||||
{
|
||||
link: 'user/profile',
|
||||
params: {},
|
||||
mandatory: true,
|
||||
},
|
||||
]}
|
||||
renderFunction={this.getScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ProfileScreen);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// @flow
|
||||
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
export type Coordinates = {
|
||||
x: number,
|
||||
y: number,
|
||||
}
|
||||
export type CoordinatesType = {
|
||||
x: number,
|
||||
y: number,
|
||||
};
|
||||
|
||||
type Shape = Array<Array<number>>;
|
||||
export type ShapeType = Array<Array<number>>;
|
||||
|
||||
/**
|
||||
* Abstract class used to represent a BaseShape.
|
||||
|
|
@ -15,96 +15,98 @@ type Shape = Array<Array<number>>;
|
|||
* and in methods to implement
|
||||
*/
|
||||
export default class BaseShape {
|
||||
#currentShape: ShapeType;
|
||||
|
||||
#currentShape: Shape;
|
||||
#rotation: number;
|
||||
position: Coordinates;
|
||||
theme: CustomTheme;
|
||||
#rotation: number;
|
||||
|
||||
/**
|
||||
* Prevent instantiation if classname is BaseShape to force class to be abstract
|
||||
*/
|
||||
constructor(theme: CustomTheme) {
|
||||
if (this.constructor === BaseShape)
|
||||
throw new Error("Abstract class can't be instantiated");
|
||||
this.theme = theme;
|
||||
this.#rotation = 0;
|
||||
this.position = {x: 0, y: 0};
|
||||
this.#currentShape = this.getShapes()[this.#rotation];
|
||||
}
|
||||
position: CoordinatesType;
|
||||
|
||||
/**
|
||||
* Gets this shape's color.
|
||||
* Must be implemented by child class
|
||||
*/
|
||||
getColor(): string {
|
||||
throw new Error("Method 'getColor()' must be implemented");
|
||||
}
|
||||
theme: CustomThemeType;
|
||||
|
||||
/**
|
||||
* Gets this object's all possible shapes as an array.
|
||||
* Must be implemented by child class.
|
||||
*
|
||||
* Used by tests to read private fields
|
||||
*/
|
||||
getShapes(): Array<Shape> {
|
||||
throw new Error("Method 'getShapes()' must be implemented");
|
||||
}
|
||||
/**
|
||||
* Prevent instantiation if classname is BaseShape to force class to be abstract
|
||||
*/
|
||||
constructor(theme: CustomThemeType) {
|
||||
if (this.constructor === BaseShape)
|
||||
throw new Error("Abstract class can't be instantiated");
|
||||
this.theme = theme;
|
||||
this.#rotation = 0;
|
||||
this.position = {x: 0, y: 0};
|
||||
this.#currentShape = this.getShapes()[this.#rotation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this object's current shape.
|
||||
*/
|
||||
getCurrentShape(): Shape {
|
||||
return this.#currentShape;
|
||||
}
|
||||
/**
|
||||
* Gets this shape's color.
|
||||
* Must be implemented by child class
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getColor(): string {
|
||||
throw new Error("Method 'getColor()' must be implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this object's coordinates.
|
||||
* This will return an array of coordinates representing the positions of the cells used by this object.
|
||||
*
|
||||
* @param isAbsolute Should we take into account the current position of the object?
|
||||
* @return {Array<Coordinates>} This object cells coordinates
|
||||
*/
|
||||
getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> {
|
||||
let coordinates = [];
|
||||
for (let row = 0; row < this.#currentShape.length; row++) {
|
||||
for (let col = 0; col < this.#currentShape[row].length; col++) {
|
||||
if (this.#currentShape[row][col] === 1)
|
||||
if (isAbsolute)
|
||||
coordinates.push({x: this.position.x + col, y: this.position.y + row});
|
||||
else
|
||||
coordinates.push({x: col, y: row});
|
||||
}
|
||||
/**
|
||||
* Gets this object's all possible shapes as an array.
|
||||
* Must be implemented by child class.
|
||||
*
|
||||
* Used by tests to read private fields
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
throw new Error("Method 'getShapes()' must be implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this object's current shape.
|
||||
*/
|
||||
getCurrentShape(): ShapeType {
|
||||
return this.#currentShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this object's coordinates.
|
||||
* This will return an array of coordinates representing the positions of the cells used by this object.
|
||||
*
|
||||
* @param isAbsolute Should we take into account the current position of the object?
|
||||
* @return {Array<CoordinatesType>} This object cells coordinates
|
||||
*/
|
||||
getCellsCoordinates(isAbsolute: boolean): Array<CoordinatesType> {
|
||||
const coordinates = [];
|
||||
for (let row = 0; row < this.#currentShape.length; row += 1) {
|
||||
for (let col = 0; col < this.#currentShape[row].length; col += 1) {
|
||||
if (this.#currentShape[row][col] === 1) {
|
||||
if (isAbsolute) {
|
||||
coordinates.push({
|
||||
x: this.position.x + col,
|
||||
y: this.position.y + row,
|
||||
});
|
||||
} else coordinates.push({x: col, y: row});
|
||||
}
|
||||
return coordinates;
|
||||
}
|
||||
}
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate this object
|
||||
*
|
||||
* @param isForward Should we rotate clockwise?
|
||||
*/
|
||||
rotate(isForward: boolean) {
|
||||
if (isForward)
|
||||
this.#rotation++;
|
||||
else
|
||||
this.#rotation--;
|
||||
if (this.#rotation > 3)
|
||||
this.#rotation = 0;
|
||||
else if (this.#rotation < 0)
|
||||
this.#rotation = 3;
|
||||
this.#currentShape = this.getShapes()[this.#rotation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this object
|
||||
*
|
||||
* @param x Position X offset to add
|
||||
* @param y Position Y offset to add
|
||||
*/
|
||||
move(x: number, y: number) {
|
||||
this.position.x += x;
|
||||
this.position.y += y;
|
||||
}
|
||||
/**
|
||||
* Rotate this object
|
||||
*
|
||||
* @param isForward Should we rotate clockwise?
|
||||
*/
|
||||
rotate(isForward: boolean) {
|
||||
if (isForward) this.#rotation += 1;
|
||||
else this.#rotation -= 1;
|
||||
if (this.#rotation > 3) this.#rotation = 0;
|
||||
else if (this.#rotation < 0) this.#rotation = 3;
|
||||
this.#currentShape = this.getShapes()[this.#rotation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this object
|
||||
*
|
||||
* @param x Position X offset to add
|
||||
* @param y Position Y offset to add
|
||||
*/
|
||||
move(x: number, y: number) {
|
||||
this.position.x += x;
|
||||
this.position.y += y;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,46 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeI extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisI;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisI;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeJ extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisJ;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisJ;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 1],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 1],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeL extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisL;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisL;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 0, 0],
|
||||
],
|
||||
[
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 0, 0],
|
||||
],
|
||||
[
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,38 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeO extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 4;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 4;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisO;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisO;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
[
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeS extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisS;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisS;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 1],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
],
|
||||
[
|
||||
[1, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 1],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
],
|
||||
[
|
||||
[1, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeT extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisT;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisT;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import BaseShape from "./BaseShape";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import BaseShape from './BaseShape';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ShapeType} from './BaseShape';
|
||||
|
||||
export default class ShapeZ extends BaseShape {
|
||||
constructor(theme: CustomThemeType) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
|
||||
constructor(theme: CustomTheme) {
|
||||
super(theme);
|
||||
this.position.x = 3;
|
||||
}
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisZ;
|
||||
}
|
||||
|
||||
getColor(): string {
|
||||
return this.theme.colors.tetrisZ;
|
||||
}
|
||||
|
||||
getShapes() {
|
||||
return [
|
||||
[
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 1],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
[1, 0, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getShapes(): Array<ShapeType> {
|
||||
return [
|
||||
[
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 1],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
],
|
||||
[
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
[1, 0, 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,106 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import GridManager from "../logic/GridManager";
|
||||
import ScoreManager from "../logic/ScoreManager";
|
||||
import Piece from "../logic/Piece";
|
||||
import GridManager from '../logic/GridManager';
|
||||
import ScoreManager from '../logic/ScoreManager';
|
||||
import Piece from '../logic/Piece';
|
||||
|
||||
let colors = {
|
||||
tetrisBackground: "#000002"
|
||||
tetrisBackground: '#000002',
|
||||
};
|
||||
|
||||
jest.mock("../ScoreManager");
|
||||
jest.mock('../ScoreManager');
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
||||
test('getEmptyLine', () => {
|
||||
let g = new GridManager(2, 2, colors);
|
||||
expect(g.getEmptyLine(2)).toStrictEqual([
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
]);
|
||||
let g = new GridManager(2, 2, colors);
|
||||
expect(g.getEmptyLine(2)).toStrictEqual([
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
]);
|
||||
|
||||
expect(g.getEmptyLine(-1)).toStrictEqual([]);
|
||||
expect(g.getEmptyLine(-1)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test('getEmptyGrid', () => {
|
||||
let g = new GridManager(2, 2, colors);
|
||||
expect(g.getEmptyGrid(2, 2)).toStrictEqual([
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
]);
|
||||
let g = new GridManager(2, 2, colors);
|
||||
expect(g.getEmptyGrid(2, 2)).toStrictEqual([
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(g.getEmptyGrid(-1, 2)).toStrictEqual([]);
|
||||
expect(g.getEmptyGrid(2, -1)).toStrictEqual([[], []]);
|
||||
expect(g.getEmptyGrid(-1, 2)).toStrictEqual([]);
|
||||
expect(g.getEmptyGrid(2, -1)).toStrictEqual([[], []]);
|
||||
});
|
||||
|
||||
test('getLinesToClear', () => {
|
||||
let g = new GridManager(2, 2, colors);
|
||||
g.getCurrentGrid()[0][0].isEmpty = false;
|
||||
g.getCurrentGrid()[0][1].isEmpty = false;
|
||||
let coord = [{x: 1, y: 0}];
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([0]);
|
||||
let g = new GridManager(2, 2, colors);
|
||||
g.getCurrentGrid()[0][0].isEmpty = false;
|
||||
g.getCurrentGrid()[0][1].isEmpty = false;
|
||||
let coord = [{x: 1, y: 0}];
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([0]);
|
||||
|
||||
g.getCurrentGrid()[0][0].isEmpty = true;
|
||||
g.getCurrentGrid()[0][1].isEmpty = true;
|
||||
g.getCurrentGrid()[1][0].isEmpty = false;
|
||||
g.getCurrentGrid()[1][1].isEmpty = false;
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([]);
|
||||
coord = [{x: 1, y: 1}];
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([1]);
|
||||
g.getCurrentGrid()[0][0].isEmpty = true;
|
||||
g.getCurrentGrid()[0][1].isEmpty = true;
|
||||
g.getCurrentGrid()[1][0].isEmpty = false;
|
||||
g.getCurrentGrid()[1][1].isEmpty = false;
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([]);
|
||||
coord = [{x: 1, y: 1}];
|
||||
expect(g.getLinesToClear(coord)).toStrictEqual([1]);
|
||||
});
|
||||
|
||||
test('clearLines', () => {
|
||||
let g = new GridManager(2, 2, colors);
|
||||
let grid = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: '0', isEmpty: true},
|
||||
{color: '0', isEmpty: true},
|
||||
],
|
||||
];
|
||||
g.getCurrentGrid()[1][0].color = '0';
|
||||
g.getCurrentGrid()[1][1].color = '0';
|
||||
expect(g.getCurrentGrid()).toStrictEqual(grid);
|
||||
let scoreManager = new ScoreManager();
|
||||
g.clearLines([1], scoreManager);
|
||||
grid = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
expect(g.getCurrentGrid()).toStrictEqual(grid);
|
||||
let g = new GridManager(2, 2, colors);
|
||||
let grid = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: '0', isEmpty: true},
|
||||
{color: '0', isEmpty: true},
|
||||
],
|
||||
];
|
||||
g.getCurrentGrid()[1][0].color = '0';
|
||||
g.getCurrentGrid()[1][1].color = '0';
|
||||
expect(g.getCurrentGrid()).toStrictEqual(grid);
|
||||
let scoreManager = new ScoreManager();
|
||||
g.clearLines([1], scoreManager);
|
||||
grid = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
expect(g.getCurrentGrid()).toStrictEqual(grid);
|
||||
});
|
||||
|
||||
test('freezeTetromino', () => {
|
||||
let g = new GridManager(2, 2, colors);
|
||||
let spy1 = jest.spyOn(GridManager.prototype, 'getLinesToClear')
|
||||
.mockImplementation(() => {});
|
||||
let spy2 = jest.spyOn(GridManager.prototype, 'clearLines')
|
||||
.mockImplementation(() => {});
|
||||
g.freezeTetromino(new Piece({}), null);
|
||||
let g = new GridManager(2, 2, colors);
|
||||
let spy1 = jest
|
||||
.spyOn(GridManager.prototype, 'getLinesToClear')
|
||||
.mockImplementation(() => {});
|
||||
let spy2 = jest
|
||||
.spyOn(GridManager.prototype, 'clearLines')
|
||||
.mockImplementation(() => {});
|
||||
g.freezeTetromino(new Piece({}), null);
|
||||
|
||||
expect(spy1).toHaveBeenCalled();
|
||||
expect(spy2).toHaveBeenCalled();
|
||||
expect(spy1).toHaveBeenCalled();
|
||||
expect(spy2).toHaveBeenCalled();
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,155 +1,186 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import Piece from "../logic/Piece";
|
||||
import ShapeI from "../Shapes/ShapeI";
|
||||
import Piece from '../logic/Piece';
|
||||
import ShapeI from '../Shapes/ShapeI';
|
||||
|
||||
let colors = {
|
||||
tetrisI: "#000001",
|
||||
tetrisBackground: "#000002"
|
||||
tetrisI: '#000001',
|
||||
tetrisBackground: '#000002',
|
||||
};
|
||||
|
||||
jest.mock("../Shapes/ShapeI");
|
||||
jest.mock('../Shapes/ShapeI');
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(Piece.prototype, 'getRandomShape')
|
||||
.mockImplementation((colors: Object) => {return new ShapeI(colors);});
|
||||
jest
|
||||
.spyOn(Piece.prototype, 'getRandomShape')
|
||||
.mockImplementation((colors: Object) => {
|
||||
return new ShapeI(colors);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('isPositionValid', () => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {return [{x: x, y: y}];});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: false}],
|
||||
];
|
||||
let size = 2;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy = jest
|
||||
.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {
|
||||
return [{x: x, y: y}];
|
||||
});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: false}],
|
||||
];
|
||||
let size = 2;
|
||||
|
||||
let p = new Piece(colors);
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 0; y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1; y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 2; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = -1; y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0; y = 2;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0; y = -1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
let p = new Piece(colors);
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1;
|
||||
y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 0;
|
||||
y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeTrue();
|
||||
x = 1;
|
||||
y = 1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 2;
|
||||
y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = -1;
|
||||
y = 0;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0;
|
||||
y = 2;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
x = 0;
|
||||
y = -1;
|
||||
expect(p.isPositionValid(grid, size, size)).toBeFalse();
|
||||
|
||||
spy.mockRestore();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('tryMove', () => {
|
||||
let p = new Piece(colors);
|
||||
const callbackMock = jest.fn();
|
||||
let isValid = true;
|
||||
let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {return isValid;});
|
||||
let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid')
|
||||
.mockImplementation(() => {});
|
||||
let spy3 = jest.spyOn(Piece.prototype, 'toGrid')
|
||||
.mockImplementation(() => {});
|
||||
let p = new Piece(colors);
|
||||
const callbackMock = jest.fn();
|
||||
let isValid = true;
|
||||
let spy1 = jest
|
||||
.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {
|
||||
return isValid;
|
||||
});
|
||||
let spy2 = jest
|
||||
.spyOn(Piece.prototype, 'removeFromGrid')
|
||||
.mockImplementation(() => {});
|
||||
let spy3 = jest.spyOn(Piece.prototype, 'toGrid').mockImplementation(() => {});
|
||||
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeFalse();
|
||||
isValid = true;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeTrue();
|
||||
expect(callbackMock).toBeCalledTimes(0);
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeFalse();
|
||||
isValid = true;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeTrue();
|
||||
expect(callbackMock).toBeCalledTimes(0);
|
||||
|
||||
isValid = false;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse();
|
||||
expect(callbackMock).toBeCalledTimes(1);
|
||||
isValid = false;
|
||||
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse();
|
||||
expect(callbackMock).toBeCalledTimes(1);
|
||||
|
||||
expect(spy2).toBeCalledTimes(4);
|
||||
expect(spy3).toBeCalledTimes(4);
|
||||
expect(spy2).toBeCalledTimes(4);
|
||||
expect(spy3).toBeCalledTimes(4);
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy3.mockRestore();
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy3.mockRestore();
|
||||
});
|
||||
|
||||
test('tryRotate', () => {
|
||||
let p = new Piece(colors);
|
||||
let isValid = true;
|
||||
let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {return isValid;});
|
||||
let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid')
|
||||
.mockImplementation(() => {});
|
||||
let spy3 = jest.spyOn(Piece.prototype, 'toGrid')
|
||||
.mockImplementation(() => {});
|
||||
let p = new Piece(colors);
|
||||
let isValid = true;
|
||||
let spy1 = jest
|
||||
.spyOn(Piece.prototype, 'isPositionValid')
|
||||
.mockImplementation(() => {
|
||||
return isValid;
|
||||
});
|
||||
let spy2 = jest
|
||||
.spyOn(Piece.prototype, 'removeFromGrid')
|
||||
.mockImplementation(() => {});
|
||||
let spy3 = jest.spyOn(Piece.prototype, 'toGrid').mockImplementation(() => {});
|
||||
|
||||
expect(p.tryRotate( null, null, null)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryRotate( null, null, null)).toBeFalse();
|
||||
expect(p.tryRotate(null, null, null)).toBeTrue();
|
||||
isValid = false;
|
||||
expect(p.tryRotate(null, null, null)).toBeFalse();
|
||||
|
||||
expect(spy2).toBeCalledTimes(2);
|
||||
expect(spy3).toBeCalledTimes(2);
|
||||
expect(spy2).toBeCalledTimes(2);
|
||||
expect(spy3).toBeCalledTimes(2);
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy3.mockRestore();
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy3.mockRestore();
|
||||
});
|
||||
|
||||
|
||||
test('toGrid', () => {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {return [{x: x, y: y}];});
|
||||
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor')
|
||||
.mockImplementation(() => {return colors.tetrisI;});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
let expectedGrid = [
|
||||
[{color: colors.tetrisI, isEmpty: false}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let spy1 = jest
|
||||
.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {
|
||||
return [{x: x, y: y}];
|
||||
});
|
||||
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor').mockImplementation(() => {
|
||||
return colors.tetrisI;
|
||||
});
|
||||
let grid = [
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
let expectedGrid = [
|
||||
[{color: colors.tetrisI, isEmpty: false}, {isEmpty: true}],
|
||||
[{isEmpty: true}, {isEmpty: true}],
|
||||
];
|
||||
|
||||
let p = new Piece(colors);
|
||||
p.toGrid(grid, true);
|
||||
expect(grid).toStrictEqual(expectedGrid);
|
||||
let p = new Piece(colors);
|
||||
p.toGrid(grid, true);
|
||||
expect(grid).toStrictEqual(expectedGrid);
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
||||
test('removeFromGrid', () => {
|
||||
let gridOld = [
|
||||
[
|
||||
{color: colors.tetrisI, isEmpty: false},
|
||||
{color: colors.tetrisI, isEmpty: false},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
let gridNew = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
let oldCoord = [{x: 0, y: 0}, {x: 1, y: 0}];
|
||||
let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {return oldCoord;});
|
||||
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor')
|
||||
.mockImplementation(() => {return colors.tetrisI;});
|
||||
let p = new Piece(colors);
|
||||
p.removeFromGrid(gridOld);
|
||||
expect(gridOld).toStrictEqual(gridNew);
|
||||
let gridOld = [
|
||||
[
|
||||
{color: colors.tetrisI, isEmpty: false},
|
||||
{color: colors.tetrisI, isEmpty: false},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
let gridNew = [
|
||||
[
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
{color: colors.tetrisBackground, isEmpty: true},
|
||||
],
|
||||
];
|
||||
let oldCoord = [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
];
|
||||
let spy1 = jest
|
||||
.spyOn(ShapeI.prototype, 'getCellsCoordinates')
|
||||
.mockImplementation(() => {
|
||||
return oldCoord;
|
||||
});
|
||||
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor').mockImplementation(() => {
|
||||
return colors.tetrisI;
|
||||
});
|
||||
let p = new Piece(colors);
|
||||
p.removeFromGrid(gridOld);
|
||||
expect(gridOld).toStrictEqual(gridNew);
|
||||
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
spy1.mockRestore();
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,71 +1,72 @@
|
|||
import React from 'react';
|
||||
import ScoreManager from "../logic/ScoreManager";
|
||||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import ScoreManager from '../logic/ScoreManager';
|
||||
|
||||
test('incrementScore', () => {
|
||||
let scoreManager = new ScoreManager();
|
||||
expect(scoreManager.getScore()).toBe(0);
|
||||
scoreManager.incrementScore();
|
||||
expect(scoreManager.getScore()).toBe(1);
|
||||
let scoreManager = new ScoreManager();
|
||||
expect(scoreManager.getScore()).toBe(0);
|
||||
scoreManager.incrementScore();
|
||||
expect(scoreManager.getScore()).toBe(1);
|
||||
});
|
||||
|
||||
test('addLinesRemovedPoints', () => {
|
||||
let scoreManager = new ScoreManager();
|
||||
scoreManager.addLinesRemovedPoints(0);
|
||||
scoreManager.addLinesRemovedPoints(5);
|
||||
expect(scoreManager.getScore()).toBe(0);
|
||||
expect(scoreManager.getLevelProgression()).toBe(0);
|
||||
let scoreManager = new ScoreManager();
|
||||
scoreManager.addLinesRemovedPoints(0);
|
||||
scoreManager.addLinesRemovedPoints(5);
|
||||
expect(scoreManager.getScore()).toBe(0);
|
||||
expect(scoreManager.getLevelProgression()).toBe(0);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.getScore()).toBe(40);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.getScore()).toBe(40);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.getScore()).toBe(140);
|
||||
expect(scoreManager.getLevelProgression()).toBe(4);
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.getScore()).toBe(140);
|
||||
expect(scoreManager.getLevelProgression()).toBe(4);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(3);
|
||||
expect(scoreManager.getScore()).toBe(440);
|
||||
expect(scoreManager.getLevelProgression()).toBe(9);
|
||||
scoreManager.addLinesRemovedPoints(3);
|
||||
expect(scoreManager.getScore()).toBe(440);
|
||||
expect(scoreManager.getLevelProgression()).toBe(9);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(4);
|
||||
expect(scoreManager.getScore()).toBe(1640);
|
||||
expect(scoreManager.getLevelProgression()).toBe(17);
|
||||
scoreManager.addLinesRemovedPoints(4);
|
||||
expect(scoreManager.getScore()).toBe(1640);
|
||||
expect(scoreManager.getLevelProgression()).toBe(17);
|
||||
});
|
||||
|
||||
test('canLevelUp', () => {
|
||||
let scoreManager = new ScoreManager();
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(0);
|
||||
expect(scoreManager.getLevelProgression()).toBe(0);
|
||||
let scoreManager = new ScoreManager();
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(0);
|
||||
expect(scoreManager.getLevelProgression()).toBe(0);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(2);
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(2);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(5);
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(1);
|
||||
expect(scoreManager.getLevelProgression()).toBe(5);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(2);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
scoreManager.addLinesRemovedPoints(1);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(2);
|
||||
expect(scoreManager.getLevelProgression()).toBe(1);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(4);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(2);
|
||||
expect(scoreManager.getLevelProgression()).toBe(9);
|
||||
scoreManager.addLinesRemovedPoints(4);
|
||||
expect(scoreManager.canLevelUp()).toBeFalse();
|
||||
expect(scoreManager.getLevel()).toBe(2);
|
||||
expect(scoreManager.getLevelProgression()).toBe(9);
|
||||
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(3);
|
||||
expect(scoreManager.getLevelProgression()).toBe(2);
|
||||
scoreManager.addLinesRemovedPoints(2);
|
||||
expect(scoreManager.canLevelUp()).toBeTrue();
|
||||
expect(scoreManager.getLevel()).toBe(3);
|
||||
expect(scoreManager.getLevelProgression()).toBe(2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,104 +1,106 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import BaseShape from "../Shapes/BaseShape";
|
||||
import ShapeI from "../Shapes/ShapeI";
|
||||
import BaseShape from '../Shapes/BaseShape';
|
||||
import ShapeI from '../Shapes/ShapeI';
|
||||
|
||||
const colors = {
|
||||
tetrisI: '#000001',
|
||||
tetrisO: '#000002',
|
||||
tetrisT: '#000003',
|
||||
tetrisS: '#000004',
|
||||
tetrisZ: '#000005',
|
||||
tetrisJ: '#000006',
|
||||
tetrisL: '#000007',
|
||||
tetrisI: '#000001',
|
||||
tetrisO: '#000002',
|
||||
tetrisT: '#000003',
|
||||
tetrisS: '#000004',
|
||||
tetrisZ: '#000005',
|
||||
tetrisJ: '#000006',
|
||||
tetrisL: '#000007',
|
||||
};
|
||||
|
||||
test('constructor', () => {
|
||||
expect(() => new BaseShape()).toThrow(Error);
|
||||
expect(() => new BaseShape()).toThrow(Error);
|
||||
|
||||
let T = new ShapeI(colors);
|
||||
expect(T.position.y).toBe(0);
|
||||
expect(T.position.x).toBe(3);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
expect(T.getColor()).toBe(colors.tetrisI);
|
||||
let T = new ShapeI(colors);
|
||||
expect(T.position.y).toBe(0);
|
||||
expect(T.position.x).toBe(3);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
expect(T.getColor()).toBe(colors.tetrisI);
|
||||
});
|
||||
|
||||
test("move", () => {
|
||||
let T = new ShapeI(colors);
|
||||
T.move(0, 1);
|
||||
expect(T.position.x).toBe(3);
|
||||
expect(T.position.y).toBe(1);
|
||||
T.move(1, 0);
|
||||
expect(T.position.x).toBe(4);
|
||||
expect(T.position.y).toBe(1);
|
||||
T.move(1, 1);
|
||||
expect(T.position.x).toBe(5);
|
||||
expect(T.position.y).toBe(2);
|
||||
T.move(2, 2);
|
||||
expect(T.position.x).toBe(7);
|
||||
expect(T.position.y).toBe(4);
|
||||
T.move(-1, -1);
|
||||
expect(T.position.x).toBe(6);
|
||||
expect(T.position.y).toBe(3);
|
||||
test('move', () => {
|
||||
let T = new ShapeI(colors);
|
||||
T.move(0, 1);
|
||||
expect(T.position.x).toBe(3);
|
||||
expect(T.position.y).toBe(1);
|
||||
T.move(1, 0);
|
||||
expect(T.position.x).toBe(4);
|
||||
expect(T.position.y).toBe(1);
|
||||
T.move(1, 1);
|
||||
expect(T.position.x).toBe(5);
|
||||
expect(T.position.y).toBe(2);
|
||||
T.move(2, 2);
|
||||
expect(T.position.x).toBe(7);
|
||||
expect(T.position.y).toBe(4);
|
||||
T.move(-1, -1);
|
||||
expect(T.position.x).toBe(6);
|
||||
expect(T.position.y).toBe(3);
|
||||
});
|
||||
|
||||
test('rotate', () => {
|
||||
let T = new ShapeI(colors);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
let T = new ShapeI(colors);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
|
||||
T.rotate(true);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
|
||||
T.rotate(false);
|
||||
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
|
||||
});
|
||||
|
||||
test('getCellsCoordinates', () => {
|
||||
let T = new ShapeI(colors);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 1},
|
||||
{x: 3, y: 1},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 3, y: 1},
|
||||
{x: 4, y: 1},
|
||||
{x: 5, y: 1},
|
||||
{x: 6, y: 1},
|
||||
]);
|
||||
T.move(1, 1);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 1},
|
||||
{x: 3, y: 1},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 4, y: 2},
|
||||
{x: 5, y: 2},
|
||||
{x: 6, y: 2},
|
||||
{x: 7, y: 2},
|
||||
]);
|
||||
T.rotate(true);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 2, y: 0},
|
||||
{x: 2, y: 1},
|
||||
{x: 2, y: 2},
|
||||
{x: 2, y: 3},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 6, y: 1},
|
||||
{x: 6, y: 2},
|
||||
{x: 6, y: 3},
|
||||
{x: 6, y: 4},
|
||||
]);
|
||||
let T = new ShapeI(colors);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 1},
|
||||
{x: 3, y: 1},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 3, y: 1},
|
||||
{x: 4, y: 1},
|
||||
{x: 5, y: 1},
|
||||
{x: 6, y: 1},
|
||||
]);
|
||||
T.move(1, 1);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 0, y: 1},
|
||||
{x: 1, y: 1},
|
||||
{x: 2, y: 1},
|
||||
{x: 3, y: 1},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 4, y: 2},
|
||||
{x: 5, y: 2},
|
||||
{x: 6, y: 2},
|
||||
{x: 7, y: 2},
|
||||
]);
|
||||
T.rotate(true);
|
||||
expect(T.getCellsCoordinates(false)).toStrictEqual([
|
||||
{x: 2, y: 0},
|
||||
{x: 2, y: 1},
|
||||
{x: 2, y: 2},
|
||||
{x: 2, y: 3},
|
||||
]);
|
||||
expect(T.getCellsCoordinates(true)).toStrictEqual([
|
||||
{x: 6, y: 1},
|
||||
{x: 6, y: 2},
|
||||
{x: 6, y: 3},
|
||||
{x: 6, y: 4},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,34 +3,30 @@
|
|||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
|
||||
export type Cell = {color: string, isEmpty: boolean, key: string};
|
||||
|
||||
type Props = {
|
||||
cell: Cell,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
class CellComponent extends React.PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const item = this.props.cell;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: item.isEmpty ? 'transparent' : item.color,
|
||||
borderColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export type CellType = {color: string, isEmpty: boolean, key: string};
|
||||
|
||||
type PropsType = {
|
||||
cell: CellType,
|
||||
};
|
||||
|
||||
class CellComponent extends React.PureComponent<PropsType> {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const item = props.cell;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: item.isEmpty ? 'transparent' : item.color,
|
||||
borderColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(CellComponent);
|
||||
|
|
|
|||
|
|
@ -3,56 +3,55 @@
|
|||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import type {Cell} from "./CellComponent";
|
||||
import CellComponent from "./CellComponent";
|
||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
import type {CellType} from './CellComponent';
|
||||
import CellComponent from './CellComponent';
|
||||
|
||||
export type Grid = Array<Array<CellComponent>>;
|
||||
export type GridType = Array<Array<CellComponent>>;
|
||||
|
||||
type Props = {
|
||||
grid: Array<Array<Object>>,
|
||||
height: number,
|
||||
width: number,
|
||||
style: ViewStyle,
|
||||
}
|
||||
type PropsType = {
|
||||
grid: Array<Array<CellType>>,
|
||||
height: number,
|
||||
width: number,
|
||||
style: ViewStyle,
|
||||
};
|
||||
|
||||
class GridComponent extends React.Component<Props> {
|
||||
class GridComponent extends React.Component<PropsType> {
|
||||
getRow(rowNumber: number): React.Node {
|
||||
const {grid} = this.props;
|
||||
return (
|
||||
<View style={{flexDirection: 'row'}} key={rowNumber.toString()}>
|
||||
{grid[rowNumber].map(this.getCellRender)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getRow(rowNumber: number) {
|
||||
let cells = this.props.grid[rowNumber].map(this.getCellRender);
|
||||
return (
|
||||
<View
|
||||
style={{flexDirection: 'row',}}
|
||||
key={rowNumber.toString()}
|
||||
>
|
||||
{cells}
|
||||
</View>
|
||||
);
|
||||
getCellRender = (item: CellType): React.Node => {
|
||||
return <CellComponent cell={item} key={item.key} />;
|
||||
};
|
||||
|
||||
getGrid(): React.Node {
|
||||
const {height} = this.props;
|
||||
const rows = [];
|
||||
for (let i = 0; i < height; i += 1) {
|
||||
rows.push(this.getRow(i));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
getCellRender = (item: Cell) => {
|
||||
return <CellComponent cell={item} key={item.key}/>;
|
||||
};
|
||||
|
||||
getGrid() {
|
||||
let rows = [];
|
||||
for (let i = 0; i < this.props.height; i++) {
|
||||
rows.push(this.getRow(i));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{
|
||||
aspectRatio: this.props.width / this.props.height,
|
||||
borderRadius: 4,
|
||||
...this.props.style
|
||||
}}>
|
||||
{this.getGrid()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {style, width, height} = this.props;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
aspectRatio: width / height,
|
||||
borderRadius: 4,
|
||||
...style,
|
||||
}}>
|
||||
{this.getGrid()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(GridComponent);
|
||||
|
|
|
|||
|
|
@ -3,51 +3,48 @@
|
|||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import type {Grid} from "./GridComponent";
|
||||
import GridComponent from "./GridComponent";
|
||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
import type {GridType} from './GridComponent';
|
||||
import GridComponent from './GridComponent';
|
||||
|
||||
type Props = {
|
||||
items: Array<Grid>,
|
||||
style: ViewStyle
|
||||
}
|
||||
type PropsType = {
|
||||
items: Array<GridType>,
|
||||
style: ViewStyle,
|
||||
};
|
||||
|
||||
class Preview extends React.PureComponent<Props> {
|
||||
class Preview extends React.PureComponent<PropsType> {
|
||||
getGrids(): React.Node {
|
||||
const {items} = this.props;
|
||||
const grids = [];
|
||||
items.forEach((item: GridType, index: number) => {
|
||||
grids.push(Preview.getGridRender(item, index));
|
||||
});
|
||||
return grids;
|
||||
}
|
||||
|
||||
getGrids() {
|
||||
let grids = [];
|
||||
for (let i = 0; i < this.props.items.length; i++) {
|
||||
grids.push(this.getGridRender(this.props.items[i], i));
|
||||
}
|
||||
return grids;
|
||||
static getGridRender(item: GridType, index: number): React.Node {
|
||||
return (
|
||||
<GridComponent
|
||||
width={item[0].length}
|
||||
height={item.length}
|
||||
grid={item}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
key={index.toString()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {style, items} = this.props;
|
||||
if (items.length > 0) {
|
||||
return <View style={style}>{this.getGrids()}</View>;
|
||||
}
|
||||
|
||||
getGridRender(item: Grid, index: number) {
|
||||
return <GridComponent
|
||||
width={item[0].length}
|
||||
height={item.length}
|
||||
grid={item}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
key={index.toString()}
|
||||
/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.items.length > 0) {
|
||||
return (
|
||||
<View style={this.props.style}>
|
||||
{this.getGrids()}
|
||||
</View>
|
||||
);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(Preview);
|
||||
|
|
|
|||
|
|
@ -1,243 +1,318 @@
|
|||
// @flow
|
||||
|
||||
import Piece from "./Piece";
|
||||
import ScoreManager from "./ScoreManager";
|
||||
import GridManager from "./GridManager";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import Piece from './Piece';
|
||||
import ScoreManager from './ScoreManager';
|
||||
import GridManager from './GridManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {GridType} from '../components/GridComponent';
|
||||
|
||||
export type TickCallbackType = (
|
||||
score: number,
|
||||
level: number,
|
||||
grid: GridType,
|
||||
) => void;
|
||||
|
||||
export type ClockCallbackType = (time: number) => void;
|
||||
|
||||
export type EndCallbackType = (
|
||||
time: number,
|
||||
score: number,
|
||||
isRestart: boolean,
|
||||
) => void;
|
||||
|
||||
export type MovementCallbackType = (grid: GridType, score?: number) => void;
|
||||
|
||||
export default class GameLogic {
|
||||
static levelTicks = [1000, 800, 600, 400, 300, 200, 150, 100];
|
||||
|
||||
static levelTicks = [
|
||||
1000,
|
||||
800,
|
||||
600,
|
||||
400,
|
||||
300,
|
||||
200,
|
||||
150,
|
||||
100,
|
||||
];
|
||||
scoreManager: ScoreManager;
|
||||
|
||||
#scoreManager: ScoreManager;
|
||||
#gridManager: GridManager;
|
||||
gridManager: GridManager;
|
||||
|
||||
#height: number;
|
||||
#width: number;
|
||||
height: number;
|
||||
|
||||
#gameRunning: boolean;
|
||||
#gamePaused: boolean;
|
||||
#gameTime: number;
|
||||
width: number;
|
||||
|
||||
#currentObject: Piece;
|
||||
gameRunning: boolean;
|
||||
|
||||
#gameTick: number;
|
||||
#gameTickInterval: IntervalID;
|
||||
#gameTimeInterval: IntervalID;
|
||||
gamePaused: boolean;
|
||||
|
||||
#pressInInterval: TimeoutID;
|
||||
#isPressedIn: boolean;
|
||||
#autoRepeatActivationDelay: number;
|
||||
#autoRepeatDelay: number;
|
||||
gameTime: number;
|
||||
|
||||
#nextPieces: Array<Piece>;
|
||||
#nextPiecesCount: number;
|
||||
currentObject: Piece;
|
||||
|
||||
#onTick: Function;
|
||||
#onClock: Function;
|
||||
endCallback: Function;
|
||||
gameTick: number;
|
||||
|
||||
#theme: CustomTheme;
|
||||
gameTickInterval: IntervalID;
|
||||
|
||||
constructor(height: number, width: number, theme: CustomTheme) {
|
||||
this.#height = height;
|
||||
this.#width = width;
|
||||
this.#gameRunning = false;
|
||||
this.#gamePaused = false;
|
||||
this.#theme = theme;
|
||||
this.#autoRepeatActivationDelay = 300;
|
||||
this.#autoRepeatDelay = 50;
|
||||
this.#nextPieces = [];
|
||||
this.#nextPiecesCount = 3;
|
||||
this.#scoreManager = new ScoreManager();
|
||||
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
|
||||
}
|
||||
gameTimeInterval: IntervalID;
|
||||
|
||||
getHeight(): number {
|
||||
return this.#height;
|
||||
}
|
||||
pressInInterval: TimeoutID;
|
||||
|
||||
getWidth(): number {
|
||||
return this.#width;
|
||||
}
|
||||
isPressedIn: boolean;
|
||||
|
||||
getCurrentGrid() {
|
||||
return this.#gridManager.getCurrentGrid();
|
||||
}
|
||||
autoRepeatActivationDelay: number;
|
||||
|
||||
isGameRunning(): boolean {
|
||||
return this.#gameRunning;
|
||||
}
|
||||
autoRepeatDelay: number;
|
||||
|
||||
isGamePaused(): boolean {
|
||||
return this.#gamePaused;
|
||||
}
|
||||
nextPieces: Array<Piece>;
|
||||
|
||||
onFreeze() {
|
||||
this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager);
|
||||
this.createTetromino();
|
||||
}
|
||||
nextPiecesCount: number;
|
||||
|
||||
setNewGameTick(level: number) {
|
||||
if (level >= GameLogic.levelTicks.length)
|
||||
return;
|
||||
this.#gameTick = GameLogic.levelTicks[level];
|
||||
clearInterval(this.#gameTickInterval);
|
||||
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
|
||||
}
|
||||
tickCallback: TickCallbackType;
|
||||
|
||||
onTick(callback: Function) {
|
||||
this.#currentObject.tryMove(0, 1,
|
||||
this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
|
||||
() => this.onFreeze());
|
||||
clockCallback: ClockCallbackType;
|
||||
|
||||
endCallback: EndCallbackType;
|
||||
|
||||
theme: CustomThemeType;
|
||||
|
||||
constructor(height: number, width: number, theme: CustomThemeType) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.theme = theme;
|
||||
this.autoRepeatActivationDelay = 300;
|
||||
this.autoRepeatDelay = 50;
|
||||
this.nextPieces = [];
|
||||
this.nextPiecesCount = 3;
|
||||
this.scoreManager = new ScoreManager();
|
||||
this.gridManager = new GridManager(
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
this.theme,
|
||||
);
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
getCurrentGrid(): GridType {
|
||||
return this.gridManager.getCurrentGrid();
|
||||
}
|
||||
|
||||
isGamePaused(): boolean {
|
||||
return this.gamePaused;
|
||||
}
|
||||
|
||||
onFreeze = () => {
|
||||
this.gridManager.freezeTetromino(this.currentObject, this.scoreManager);
|
||||
this.createTetromino();
|
||||
};
|
||||
|
||||
setNewGameTick(level: number) {
|
||||
if (level >= GameLogic.levelTicks.length) return;
|
||||
this.gameTick = GameLogic.levelTicks[level];
|
||||
this.stopTick();
|
||||
this.startTick();
|
||||
}
|
||||
|
||||
startClock() {
|
||||
this.gameTimeInterval = setInterval(() => {
|
||||
this.onClock(this.clockCallback);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
startTick() {
|
||||
this.gameTickInterval = setInterval(() => {
|
||||
this.onTick(this.tickCallback);
|
||||
}, this.gameTick);
|
||||
}
|
||||
|
||||
stopClock() {
|
||||
clearInterval(this.gameTimeInterval);
|
||||
}
|
||||
|
||||
stopTick() {
|
||||
clearInterval(this.gameTickInterval);
|
||||
}
|
||||
|
||||
stopGameTime() {
|
||||
this.stopClock();
|
||||
this.stopTick();
|
||||
}
|
||||
|
||||
startGameTime() {
|
||||
this.startClock();
|
||||
this.startTick();
|
||||
}
|
||||
|
||||
onTick(callback: TickCallbackType) {
|
||||
this.currentObject.tryMove(
|
||||
0,
|
||||
1,
|
||||
this.gridManager.getCurrentGrid(),
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
this.onFreeze,
|
||||
);
|
||||
callback(
|
||||
this.scoreManager.getScore(),
|
||||
this.scoreManager.getLevel(),
|
||||
this.gridManager.getCurrentGrid(),
|
||||
);
|
||||
if (this.scoreManager.canLevelUp())
|
||||
this.setNewGameTick(this.scoreManager.getLevel());
|
||||
}
|
||||
|
||||
onClock(callback: ClockCallbackType) {
|
||||
this.gameTime += 1;
|
||||
callback(this.gameTime);
|
||||
}
|
||||
|
||||
canUseInput(): boolean {
|
||||
return this.gameRunning && !this.gamePaused;
|
||||
}
|
||||
|
||||
rightPressed(callback: MovementCallbackType) {
|
||||
this.isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, 1, 0);
|
||||
}
|
||||
|
||||
leftPressedIn(callback: MovementCallbackType) {
|
||||
this.isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, -1, 0);
|
||||
}
|
||||
|
||||
downPressedIn(callback: MovementCallbackType) {
|
||||
this.isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, 0, 1);
|
||||
}
|
||||
|
||||
movePressedRepeat(
|
||||
isInitial: boolean,
|
||||
callback: MovementCallbackType,
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
if (!this.canUseInput() || !this.isPressedIn) return;
|
||||
const moved = this.currentObject.tryMove(
|
||||
x,
|
||||
y,
|
||||
this.gridManager.getCurrentGrid(),
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
this.onFreeze,
|
||||
);
|
||||
if (moved) {
|
||||
if (y === 1) {
|
||||
this.scoreManager.incrementScore();
|
||||
callback(
|
||||
this.#scoreManager.getScore(),
|
||||
this.#scoreManager.getLevel(),
|
||||
this.#gridManager.getCurrentGrid());
|
||||
if (this.#scoreManager.canLevelUp())
|
||||
this.setNewGameTick(this.#scoreManager.getLevel());
|
||||
}
|
||||
|
||||
onClock(callback: Function) {
|
||||
this.#gameTime++;
|
||||
callback(this.#gameTime);
|
||||
}
|
||||
|
||||
canUseInput() {
|
||||
return this.#gameRunning && !this.#gamePaused
|
||||
}
|
||||
|
||||
rightPressed(callback: Function) {
|
||||
this.#isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, 1, 0);
|
||||
}
|
||||
|
||||
leftPressedIn(callback: Function) {
|
||||
this.#isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, -1, 0);
|
||||
}
|
||||
|
||||
downPressedIn(callback: Function) {
|
||||
this.#isPressedIn = true;
|
||||
this.movePressedRepeat(true, callback, 0, 1);
|
||||
}
|
||||
|
||||
movePressedRepeat(isInitial: boolean, callback: Function, x: number, y: number) {
|
||||
if (!this.canUseInput() || !this.#isPressedIn)
|
||||
return;
|
||||
const moved = this.#currentObject.tryMove(x, y,
|
||||
this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
|
||||
() => this.onFreeze());
|
||||
if (moved) {
|
||||
if (y === 1) {
|
||||
this.#scoreManager.incrementScore();
|
||||
callback(this.#gridManager.getCurrentGrid(), this.#scoreManager.getScore());
|
||||
} else
|
||||
callback(this.#gridManager.getCurrentGrid());
|
||||
}
|
||||
this.#pressInInterval = setTimeout(() =>
|
||||
this.movePressedRepeat(false, callback, x, y),
|
||||
isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay
|
||||
this.gridManager.getCurrentGrid(),
|
||||
this.scoreManager.getScore(),
|
||||
);
|
||||
} else callback(this.gridManager.getCurrentGrid());
|
||||
}
|
||||
this.pressInInterval = setTimeout(
|
||||
() => {
|
||||
this.movePressedRepeat(false, callback, x, y);
|
||||
},
|
||||
isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay,
|
||||
);
|
||||
}
|
||||
|
||||
pressedOut() {
|
||||
this.#isPressedIn = false;
|
||||
clearTimeout(this.#pressInInterval);
|
||||
pressedOut() {
|
||||
this.isPressedIn = false;
|
||||
clearTimeout(this.pressInInterval);
|
||||
}
|
||||
|
||||
rotatePressed(callback: MovementCallbackType) {
|
||||
if (!this.canUseInput()) return;
|
||||
|
||||
if (
|
||||
this.currentObject.tryRotate(
|
||||
this.gridManager.getCurrentGrid(),
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
)
|
||||
)
|
||||
callback(this.gridManager.getCurrentGrid());
|
||||
}
|
||||
|
||||
getNextPiecesPreviews(): Array<GridType> {
|
||||
const finalArray = [];
|
||||
for (let i = 0; i < this.nextPieces.length; i += 1) {
|
||||
const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0]
|
||||
.length;
|
||||
finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize));
|
||||
this.nextPieces[i].toGrid(finalArray[i], true);
|
||||
}
|
||||
return finalArray;
|
||||
}
|
||||
|
||||
rotatePressed(callback: Function) {
|
||||
if (!this.canUseInput())
|
||||
return;
|
||||
recoverNextPiece() {
|
||||
this.currentObject = this.nextPieces.shift();
|
||||
this.generateNextPieces();
|
||||
}
|
||||
|
||||
if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
|
||||
callback(this.#gridManager.getCurrentGrid());
|
||||
generateNextPieces() {
|
||||
while (this.nextPieces.length < this.nextPiecesCount) {
|
||||
this.nextPieces.push(new Piece(this.theme));
|
||||
}
|
||||
}
|
||||
|
||||
getNextPiecesPreviews() {
|
||||
let finalArray = [];
|
||||
for (let i = 0; i < this.#nextPieces.length; i++) {
|
||||
const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length;
|
||||
finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize));
|
||||
this.#nextPieces[i].toGrid(finalArray[i], true);
|
||||
}
|
||||
createTetromino() {
|
||||
this.pressedOut();
|
||||
this.recoverNextPiece();
|
||||
if (
|
||||
!this.currentObject.isPositionValid(
|
||||
this.gridManager.getCurrentGrid(),
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
)
|
||||
)
|
||||
this.endGame(false);
|
||||
}
|
||||
|
||||
return finalArray;
|
||||
}
|
||||
togglePause() {
|
||||
if (!this.gameRunning) return;
|
||||
this.gamePaused = !this.gamePaused;
|
||||
if (this.gamePaused) this.stopGameTime();
|
||||
else this.startGameTime();
|
||||
}
|
||||
|
||||
recoverNextPiece() {
|
||||
this.#currentObject = this.#nextPieces.shift();
|
||||
this.generateNextPieces();
|
||||
}
|
||||
endGame(isRestart: boolean) {
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.stopGameTime();
|
||||
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart);
|
||||
}
|
||||
|
||||
generateNextPieces() {
|
||||
while (this.#nextPieces.length < this.#nextPiecesCount) {
|
||||
this.#nextPieces.push(new Piece(this.#theme));
|
||||
}
|
||||
}
|
||||
|
||||
createTetromino() {
|
||||
this.pressedOut();
|
||||
this.recoverNextPiece();
|
||||
if (!this.#currentObject.isPositionValid(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
|
||||
this.endGame(false);
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
if (!this.#gameRunning)
|
||||
return;
|
||||
this.#gamePaused = !this.#gamePaused;
|
||||
if (this.#gamePaused) {
|
||||
clearInterval(this.#gameTickInterval);
|
||||
clearInterval(this.#gameTimeInterval);
|
||||
} else {
|
||||
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
|
||||
this.#gameTimeInterval = setInterval(this.#onClock, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
stopGame() {
|
||||
this.#gameRunning = false;
|
||||
this.#gamePaused = false;
|
||||
clearInterval(this.#gameTickInterval);
|
||||
clearInterval(this.#gameTimeInterval);
|
||||
}
|
||||
|
||||
endGame(isRestart: boolean) {
|
||||
this.stopGame();
|
||||
this.endCallback(this.#gameTime, this.#scoreManager.getScore(), isRestart);
|
||||
}
|
||||
|
||||
startGame(tickCallback: Function, clockCallback: Function, endCallback: Function) {
|
||||
if (this.#gameRunning)
|
||||
this.endGame(true);
|
||||
this.#gameRunning = true;
|
||||
this.#gamePaused = false;
|
||||
this.#gameTime = 0;
|
||||
this.#scoreManager = new ScoreManager();
|
||||
this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()];
|
||||
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
|
||||
this.#nextPieces = [];
|
||||
this.generateNextPieces();
|
||||
this.createTetromino();
|
||||
tickCallback(
|
||||
this.#scoreManager.getScore(),
|
||||
this.#scoreManager.getLevel(),
|
||||
this.#gridManager.getCurrentGrid());
|
||||
clockCallback(this.#gameTime);
|
||||
this.#onTick = this.onTick.bind(this, tickCallback);
|
||||
this.#onClock = this.onClock.bind(this, clockCallback);
|
||||
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
|
||||
this.#gameTimeInterval = setInterval(this.#onClock, 1000);
|
||||
this.endCallback = endCallback;
|
||||
}
|
||||
startGame(
|
||||
tickCallback: TickCallbackType,
|
||||
clockCallback: ClockCallbackType,
|
||||
endCallback: EndCallbackType,
|
||||
) {
|
||||
if (this.gameRunning) this.endGame(true);
|
||||
this.gameRunning = true;
|
||||
this.gamePaused = false;
|
||||
this.gameTime = 0;
|
||||
this.scoreManager = new ScoreManager();
|
||||
this.gameTick = GameLogic.levelTicks[this.scoreManager.getLevel()];
|
||||
this.gridManager = new GridManager(
|
||||
this.getWidth(),
|
||||
this.getHeight(),
|
||||
this.theme,
|
||||
);
|
||||
this.nextPieces = [];
|
||||
this.generateNextPieces();
|
||||
this.createTetromino();
|
||||
tickCallback(
|
||||
this.scoreManager.getScore(),
|
||||
this.scoreManager.getLevel(),
|
||||
this.gridManager.getCurrentGrid(),
|
||||
);
|
||||
clockCallback(this.gameTime);
|
||||
this.startTick();
|
||||
this.startClock();
|
||||
this.tickCallback = tickCallback;
|
||||
this.clockCallback = clockCallback;
|
||||
this.endCallback = endCallback;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,120 +1,122 @@
|
|||
// @flow
|
||||
|
||||
import Piece from "./Piece";
|
||||
import ScoreManager from "./ScoreManager";
|
||||
import type {Coordinates} from '../Shapes/BaseShape';
|
||||
import type {Grid} from "../components/GridComponent";
|
||||
import type {Cell} from "../components/CellComponent";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import Piece from './Piece';
|
||||
import ScoreManager from './ScoreManager';
|
||||
import type {CoordinatesType} from '../Shapes/BaseShape';
|
||||
import type {GridType} from '../components/GridComponent';
|
||||
import type {CellType} from '../components/CellComponent';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
/**
|
||||
* Class used to manage the game grid
|
||||
*/
|
||||
export default class GridManager {
|
||||
#currentGrid: GridType;
|
||||
|
||||
#currentGrid: Grid;
|
||||
#theme: CustomTheme;
|
||||
#theme: CustomThemeType;
|
||||
|
||||
/**
|
||||
* Initializes a grid of the given size
|
||||
*
|
||||
* @param width The grid width
|
||||
* @param height The grid height
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
constructor(width: number, height: number, theme: CustomTheme) {
|
||||
this.#theme = theme;
|
||||
this.#currentGrid = this.getEmptyGrid(height, width);
|
||||
/**
|
||||
* Initializes a grid of the given size
|
||||
*
|
||||
* @param width The grid width
|
||||
* @param height The grid height
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
constructor(width: number, height: number, theme: CustomThemeType) {
|
||||
this.#theme = theme;
|
||||
this.#currentGrid = this.getEmptyGrid(height, width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current grid
|
||||
*
|
||||
* @return {GridType} The current grid
|
||||
*/
|
||||
getCurrentGrid(): GridType {
|
||||
return this.#currentGrid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new empty grid line of the given size
|
||||
*
|
||||
* @param width The line size
|
||||
* @return {Array<CellType>}
|
||||
*/
|
||||
getEmptyLine(width: number): Array<CellType> {
|
||||
const line = [];
|
||||
for (let col = 0; col < width; col += 1) {
|
||||
line.push({
|
||||
color: this.#theme.colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
key: col.toString(),
|
||||
});
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current grid
|
||||
*
|
||||
* @return {Grid} The current grid
|
||||
*/
|
||||
getCurrentGrid(): Grid {
|
||||
return this.#currentGrid;
|
||||
/**
|
||||
* Gets a new empty grid
|
||||
*
|
||||
* @param width The grid width
|
||||
* @param height The grid height
|
||||
* @return {GridType} A new empty grid
|
||||
*/
|
||||
getEmptyGrid(height: number, width: number): GridType {
|
||||
const grid = [];
|
||||
for (let row = 0; row < height; row += 1) {
|
||||
grid.push(this.getEmptyLine(width));
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new empty grid line of the given size
|
||||
*
|
||||
* @param width The line size
|
||||
* @return {Array<Cell>}
|
||||
*/
|
||||
getEmptyLine(width: number): Array<Cell> {
|
||||
let line = [];
|
||||
for (let col = 0; col < width; col++) {
|
||||
line.push({
|
||||
color: this.#theme.colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
key: col.toString(),
|
||||
});
|
||||
/**
|
||||
* Removes the given lines from the grid,
|
||||
* shifts down every line on top and adds new empty lines on top.
|
||||
*
|
||||
* @param lines An array of line numbers to remove
|
||||
* @param scoreManager A reference to the score manager
|
||||
*/
|
||||
clearLines(lines: Array<number>, scoreManager: ScoreManager) {
|
||||
lines.sort();
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
this.#currentGrid.splice(lines[i], 1);
|
||||
this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length));
|
||||
}
|
||||
scoreManager.addLinesRemovedPoints(lines.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lines to clear around the given piece's coordinates.
|
||||
* The piece's coordinates are used for optimization and to prevent checking the whole grid.
|
||||
*
|
||||
* @param pos The piece's coordinates to check lines at
|
||||
* @return {Array<number>} An array containing the line numbers to clear
|
||||
*/
|
||||
getLinesToClear(pos: Array<CoordinatesType>): Array<number> {
|
||||
const rows = [];
|
||||
for (let i = 0; i < pos.length; i += 1) {
|
||||
let isLineFull = true;
|
||||
for (let col = 0; col < this.#currentGrid[pos[i].y].length; col += 1) {
|
||||
if (this.#currentGrid[pos[i].y][col].isEmpty) {
|
||||
isLineFull = false;
|
||||
break;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
if (isLineFull && rows.indexOf(pos[i].y) === -1) rows.push(pos[i].y);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new empty grid
|
||||
*
|
||||
* @param width The grid width
|
||||
* @param height The grid height
|
||||
* @return {Grid} A new empty grid
|
||||
*/
|
||||
getEmptyGrid(height: number, width: number): Grid {
|
||||
let grid = [];
|
||||
for (let row = 0; row < height; row++) {
|
||||
grid.push(this.getEmptyLine(width));
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given lines from the grid,
|
||||
* shifts down every line on top and adds new empty lines on top.
|
||||
*
|
||||
* @param lines An array of line numbers to remove
|
||||
* @param scoreManager A reference to the score manager
|
||||
*/
|
||||
clearLines(lines: Array<number>, scoreManager: ScoreManager) {
|
||||
lines.sort();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
this.#currentGrid.splice(lines[i], 1);
|
||||
this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length));
|
||||
}
|
||||
scoreManager.addLinesRemovedPoints(lines.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lines to clear around the given piece's coordinates.
|
||||
* The piece's coordinates are used for optimization and to prevent checking the whole grid.
|
||||
*
|
||||
* @param pos The piece's coordinates to check lines at
|
||||
* @return {Array<number>} An array containing the line numbers to clear
|
||||
*/
|
||||
getLinesToClear(pos: Array<Coordinates>): Array<number> {
|
||||
let rows = [];
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
let isLineFull = true;
|
||||
for (let col = 0; col < this.#currentGrid[pos[i].y].length; col++) {
|
||||
if (this.#currentGrid[pos[i].y][col].isEmpty) {
|
||||
isLineFull = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isLineFull && rows.indexOf(pos[i].y) === -1)
|
||||
rows.push(pos[i].y);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes the given piece to the grid
|
||||
*
|
||||
* @param currentObject The piece to freeze
|
||||
* @param scoreManager A reference to the score manager
|
||||
*/
|
||||
freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
|
||||
this.clearLines(this.getLinesToClear(currentObject.getCoordinates()), scoreManager);
|
||||
}
|
||||
/**
|
||||
* Freezes the given piece to the grid
|
||||
*
|
||||
* @param currentObject The piece to freeze
|
||||
* @param scoreManager A reference to the score manager
|
||||
*/
|
||||
freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) {
|
||||
this.clearLines(
|
||||
this.getLinesToClear(currentObject.getCoordinates()),
|
||||
scoreManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import ShapeL from "../Shapes/ShapeL";
|
||||
import ShapeI from "../Shapes/ShapeI";
|
||||
import ShapeJ from "../Shapes/ShapeJ";
|
||||
import ShapeO from "../Shapes/ShapeO";
|
||||
import ShapeS from "../Shapes/ShapeS";
|
||||
import ShapeT from "../Shapes/ShapeT";
|
||||
import ShapeZ from "../Shapes/ShapeZ";
|
||||
import type {Coordinates} from '../Shapes/BaseShape';
|
||||
import BaseShape from "../Shapes/BaseShape";
|
||||
import type {Grid} from "../components/GridComponent";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
// @flow
|
||||
|
||||
import ShapeL from '../Shapes/ShapeL';
|
||||
import ShapeI from '../Shapes/ShapeI';
|
||||
import ShapeJ from '../Shapes/ShapeJ';
|
||||
import ShapeO from '../Shapes/ShapeO';
|
||||
import ShapeS from '../Shapes/ShapeS';
|
||||
import ShapeT from '../Shapes/ShapeT';
|
||||
import ShapeZ from '../Shapes/ShapeZ';
|
||||
import type {CoordinatesType} from '../Shapes/BaseShape';
|
||||
import BaseShape from '../Shapes/BaseShape';
|
||||
import type {GridType} from '../components/GridComponent';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
/**
|
||||
* Class used as an abstraction layer for shapes.
|
||||
|
|
@ -16,157 +18,167 @@ import type {CustomTheme} from "../../../managers/ThemeManager";
|
|||
*
|
||||
*/
|
||||
export default class Piece {
|
||||
shapes = [ShapeL, ShapeI, ShapeJ, ShapeO, ShapeS, ShapeT, ShapeZ];
|
||||
|
||||
#shapes = [
|
||||
ShapeL,
|
||||
ShapeI,
|
||||
ShapeJ,
|
||||
ShapeO,
|
||||
ShapeS,
|
||||
ShapeT,
|
||||
ShapeZ,
|
||||
];
|
||||
#currentShape: BaseShape;
|
||||
#theme: CustomTheme;
|
||||
currentShape: BaseShape;
|
||||
|
||||
/**
|
||||
* Initializes this piece's color and shape
|
||||
*
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
constructor(theme: CustomTheme) {
|
||||
this.#currentShape = this.getRandomShape(theme);
|
||||
this.#theme = theme;
|
||||
theme: CustomThemeType;
|
||||
|
||||
/**
|
||||
* Initializes this piece's color and shape
|
||||
*
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
constructor(theme: CustomThemeType) {
|
||||
this.currentShape = this.getRandomShape(theme);
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a random shape object
|
||||
*
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
getRandomShape(theme: CustomThemeType): BaseShape {
|
||||
return new this.shapes[Math.floor(Math.random() * 7)](theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the piece from the given grid
|
||||
*
|
||||
* @param grid The grid to remove the piece from
|
||||
*/
|
||||
removeFromGrid(grid: GridType) {
|
||||
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||
true,
|
||||
);
|
||||
pos.forEach((coordinates: CoordinatesType) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
grid[coordinates.y][coordinates.x] = {
|
||||
color: this.theme.colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
key: grid[coordinates.y][coordinates.x].key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this piece to the given grid
|
||||
*
|
||||
* @param grid The grid to add the piece to
|
||||
* @param isPreview Should we use this piece's current position to determine the cells?
|
||||
*/
|
||||
toGrid(grid: GridType, isPreview: boolean) {
|
||||
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||
!isPreview,
|
||||
);
|
||||
pos.forEach((coordinates: CoordinatesType) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
grid[coordinates.y][coordinates.x] = {
|
||||
color: this.currentShape.getColor(),
|
||||
isEmpty: false,
|
||||
key: grid[coordinates.y][coordinates.x].key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the piece's current position is valid
|
||||
*
|
||||
* @param grid The current game grid
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @return {boolean} If the position is valid
|
||||
*/
|
||||
isPositionValid(grid: GridType, width: number, height: number): boolean {
|
||||
let isValid = true;
|
||||
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||
true,
|
||||
);
|
||||
for (let i = 0; i < pos.length; i += 1) {
|
||||
if (
|
||||
pos[i].x >= width ||
|
||||
pos[i].x < 0 ||
|
||||
pos[i].y >= height ||
|
||||
pos[i].y < 0 ||
|
||||
!grid[pos[i].y][pos[i].x].isEmpty
|
||||
) {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a random shape object
|
||||
*
|
||||
* @param theme Object containing current theme
|
||||
*/
|
||||
getRandomShape(theme: CustomTheme) {
|
||||
return new this.#shapes[Math.floor(Math.random() * 7)](theme);
|
||||
/**
|
||||
* Tries to move the piece by the given offset on the given grid
|
||||
*
|
||||
* @param x Position X offset
|
||||
* @param y Position Y offset
|
||||
* @param grid The grid to move the piece on
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @param freezeCallback Callback to use if the piece should freeze itself
|
||||
* @return {boolean} True if the move was valid, false otherwise
|
||||
*/
|
||||
tryMove(
|
||||
x: number,
|
||||
y: number,
|
||||
grid: GridType,
|
||||
width: number,
|
||||
height: number,
|
||||
freezeCallback: () => void,
|
||||
): boolean {
|
||||
let newX = x;
|
||||
let newY = y;
|
||||
if (x > 1) newX = 1; // Prevent moving from more than one tile
|
||||
if (x < -1) newX = -1;
|
||||
if (y > 1) newY = 1;
|
||||
if (y < -1) newY = -1;
|
||||
if (x !== 0 && y !== 0) newY = 0; // Prevent diagonal movement
|
||||
|
||||
this.removeFromGrid(grid);
|
||||
this.currentShape.move(newX, newY);
|
||||
const isValid = this.isPositionValid(grid, width, height);
|
||||
|
||||
if (!isValid) this.currentShape.move(-newX, -newY);
|
||||
|
||||
const shouldFreeze = !isValid && newY !== 0;
|
||||
this.toGrid(grid, false);
|
||||
if (shouldFreeze) freezeCallback();
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to rotate the piece
|
||||
*
|
||||
* @param grid The grid to rotate the piece on
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @return {boolean} True if the rotation was valid, false otherwise
|
||||
*/
|
||||
tryRotate(grid: GridType, width: number, height: number): boolean {
|
||||
this.removeFromGrid(grid);
|
||||
this.currentShape.rotate(true);
|
||||
if (!this.isPositionValid(grid, width, height)) {
|
||||
this.currentShape.rotate(false);
|
||||
this.toGrid(grid, false);
|
||||
return false;
|
||||
}
|
||||
this.toGrid(grid, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the piece from the given grid
|
||||
*
|
||||
* @param grid The grid to remove the piece from
|
||||
*/
|
||||
removeFromGrid(grid: Grid) {
|
||||
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
grid[pos[i].y][pos[i].x] = {
|
||||
color: this.#theme.colors.tetrisBackground,
|
||||
isEmpty: true,
|
||||
key: grid[pos[i].y][pos[i].x].key
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets this piece used cells coordinates
|
||||
*
|
||||
* @return {Array<CoordinatesType>} An array of coordinates
|
||||
*/
|
||||
getCoordinates(): Array<CoordinatesType> {
|
||||
return this.currentShape.getCellsCoordinates(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this piece to the given grid
|
||||
*
|
||||
* @param grid The grid to add the piece to
|
||||
* @param isPreview Should we use this piece's current position to determine the cells?
|
||||
*/
|
||||
toGrid(grid: Grid, isPreview: boolean) {
|
||||
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(!isPreview);
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
grid[pos[i].y][pos[i].x] = {
|
||||
color: this.#currentShape.getColor(),
|
||||
isEmpty: false,
|
||||
key: grid[pos[i].y][pos[i].x].key
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the piece's current position is valid
|
||||
*
|
||||
* @param grid The current game grid
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @return {boolean} If the position is valid
|
||||
*/
|
||||
isPositionValid(grid: Grid, width: number, height: number) {
|
||||
let isValid = true;
|
||||
const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
if (pos[i].x >= width
|
||||
|| pos[i].x < 0
|
||||
|| pos[i].y >= height
|
||||
|| pos[i].y < 0
|
||||
|| !grid[pos[i].y][pos[i].x].isEmpty) {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to move the piece by the given offset on the given grid
|
||||
*
|
||||
* @param x Position X offset
|
||||
* @param y Position Y offset
|
||||
* @param grid The grid to move the piece on
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @param freezeCallback Callback to use if the piece should freeze itself
|
||||
* @return {boolean} True if the move was valid, false otherwise
|
||||
*/
|
||||
tryMove(x: number, y: number, grid: Grid, width: number, height: number, freezeCallback: () => void) {
|
||||
if (x > 1) x = 1; // Prevent moving from more than one tile
|
||||
if (x < -1) x = -1;
|
||||
if (y > 1) y = 1;
|
||||
if (y < -1) y = -1;
|
||||
if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement
|
||||
|
||||
this.removeFromGrid(grid);
|
||||
this.#currentShape.move(x, y);
|
||||
let isValid = this.isPositionValid(grid, width, height);
|
||||
|
||||
if (!isValid)
|
||||
this.#currentShape.move(-x, -y);
|
||||
|
||||
let shouldFreeze = !isValid && y !== 0;
|
||||
this.toGrid(grid, false);
|
||||
if (shouldFreeze)
|
||||
freezeCallback();
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to rotate the piece
|
||||
*
|
||||
* @param grid The grid to rotate the piece on
|
||||
* @param width The grid's width
|
||||
* @param height The grid's height
|
||||
* @return {boolean} True if the rotation was valid, false otherwise
|
||||
*/
|
||||
tryRotate(grid: Grid, width: number, height: number) {
|
||||
this.removeFromGrid(grid);
|
||||
this.#currentShape.rotate(true);
|
||||
if (!this.isPositionValid(grid, width, height)) {
|
||||
this.#currentShape.rotate(false);
|
||||
this.toGrid(grid, false);
|
||||
return false;
|
||||
}
|
||||
this.toGrid(grid, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this piece used cells coordinates
|
||||
*
|
||||
* @return {Array<Coordinates>} An array of coordinates
|
||||
*/
|
||||
getCoordinates(): Array<Coordinates> {
|
||||
return this.#currentShape.getCellsCoordinates(true);
|
||||
}
|
||||
|
||||
getCurrentShape() {
|
||||
return this.#currentShape;
|
||||
}
|
||||
getCurrentShape(): BaseShape {
|
||||
return this.currentShape;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,98 +4,100 @@
|
|||
* Class used to manage game score
|
||||
*/
|
||||
export default class ScoreManager {
|
||||
#scoreLinesModifier = [40, 100, 300, 1200];
|
||||
|
||||
#scoreLinesModifier = [40, 100, 300, 1200];
|
||||
#score: number;
|
||||
|
||||
#score: number;
|
||||
#level: number;
|
||||
#levelProgression: number;
|
||||
#level: number;
|
||||
|
||||
/**
|
||||
* Initializes score to 0
|
||||
*/
|
||||
constructor() {
|
||||
this.#score = 0;
|
||||
this.#level = 0;
|
||||
this.#levelProgression = 0;
|
||||
#levelProgression: number;
|
||||
|
||||
/**
|
||||
* Initializes score to 0
|
||||
*/
|
||||
constructor() {
|
||||
this.#score = 0;
|
||||
this.#level = 0;
|
||||
this.#levelProgression = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current score
|
||||
*
|
||||
* @return {number} The current score
|
||||
*/
|
||||
getScore(): number {
|
||||
return this.#score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current level
|
||||
*
|
||||
* @return {number} The current level
|
||||
*/
|
||||
getLevel(): number {
|
||||
return this.#level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current level progression
|
||||
*
|
||||
* @return {number} The current level progression
|
||||
*/
|
||||
getLevelProgression(): number {
|
||||
return this.#levelProgression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the score by one
|
||||
*/
|
||||
incrementScore() {
|
||||
this.#score += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add score corresponding to the number of lines removed at the same time.
|
||||
* Also updates the level progression.
|
||||
*
|
||||
* The more lines cleared at the same time, the more points and level progression the player gets.
|
||||
*
|
||||
* @param numberRemoved The number of lines removed at the same time
|
||||
*/
|
||||
addLinesRemovedPoints(numberRemoved: number) {
|
||||
if (numberRemoved < 1 || numberRemoved > 4) return;
|
||||
this.#score +=
|
||||
this.#scoreLinesModifier[numberRemoved - 1] * (this.#level + 1);
|
||||
switch (numberRemoved) {
|
||||
case 1:
|
||||
this.#levelProgression += 1;
|
||||
break;
|
||||
case 2:
|
||||
this.#levelProgression += 3;
|
||||
break;
|
||||
case 3:
|
||||
this.#levelProgression += 5;
|
||||
break;
|
||||
case 4: // Did a tetris !
|
||||
this.#levelProgression += 8;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current score
|
||||
*
|
||||
* @return {number} The current score
|
||||
*/
|
||||
getScore(): number {
|
||||
return this.#score;
|
||||
/**
|
||||
* Checks if the player can go to the next level.
|
||||
*
|
||||
* If he can, change the level.
|
||||
*
|
||||
* @return {boolean} True if the current level has changed
|
||||
*/
|
||||
canLevelUp(): boolean {
|
||||
const canLevel = this.#levelProgression > this.#level * 5;
|
||||
if (canLevel) {
|
||||
this.#levelProgression -= this.#level * 5;
|
||||
this.#level += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current level
|
||||
*
|
||||
* @return {number} The current level
|
||||
*/
|
||||
getLevel(): number {
|
||||
return this.#level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current level progression
|
||||
*
|
||||
* @return {number} The current level progression
|
||||
*/
|
||||
getLevelProgression(): number {
|
||||
return this.#levelProgression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the score by one
|
||||
*/
|
||||
incrementScore() {
|
||||
this.#score++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add score corresponding to the number of lines removed at the same time.
|
||||
* Also updates the level progression.
|
||||
*
|
||||
* The more lines cleared at the same time, the more points and level progression the player gets.
|
||||
*
|
||||
* @param numberRemoved The number of lines removed at the same time
|
||||
*/
|
||||
addLinesRemovedPoints(numberRemoved: number) {
|
||||
if (numberRemoved < 1 || numberRemoved > 4)
|
||||
return;
|
||||
this.#score += this.#scoreLinesModifier[numberRemoved-1] * (this.#level + 1);
|
||||
switch (numberRemoved) {
|
||||
case 1:
|
||||
this.#levelProgression += 1;
|
||||
break;
|
||||
case 2:
|
||||
this.#levelProgression += 3;
|
||||
break;
|
||||
case 3:
|
||||
this.#levelProgression += 5;
|
||||
break;
|
||||
case 4: // Did a tetris !
|
||||
this.#levelProgression += 8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player can go to the next level.
|
||||
*
|
||||
* If he can, change the level.
|
||||
*
|
||||
* @return {boolean} True if the current level has changed
|
||||
*/
|
||||
canLevelUp() {
|
||||
let canLevel = this.#levelProgression > this.#level * 5;
|
||||
if (canLevel){
|
||||
this.#levelProgression -= this.#level * 5;
|
||||
this.#level++;
|
||||
}
|
||||
return canLevel;
|
||||
}
|
||||
|
||||
return canLevel;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,400 +3,447 @@
|
|||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import GameLogic from "../logic/GameLogic";
|
||||
import type {Grid} from "../components/GridComponent";
|
||||
import GridComponent from "../components/GridComponent";
|
||||
import Preview from "../components/Preview";
|
||||
import i18n from "i18n-js";
|
||||
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog";
|
||||
import OptionsDialog from "../../../components/Dialogs/OptionsDialog";
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import GameLogic from '../logic/GameLogic';
|
||||
import type {GridType} from '../components/GridComponent';
|
||||
import GridComponent from '../components/GridComponent';
|
||||
import Preview from '../components/Preview';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog';
|
||||
import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: { params: { highScore: number }, ... },
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {highScore: number}},
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type State = {
|
||||
grid: Grid,
|
||||
gameRunning: boolean,
|
||||
gameTime: number,
|
||||
gameScore: number,
|
||||
gameLevel: number,
|
||||
type StateType = {
|
||||
grid: GridType,
|
||||
gameTime: number,
|
||||
gameScore: number,
|
||||
gameLevel: number,
|
||||
|
||||
dialogVisible: boolean,
|
||||
dialogTitle: string,
|
||||
dialogMessage: string,
|
||||
dialogButtons: Array<OptionsDialogButton>,
|
||||
onDialogDismiss: () => void,
|
||||
}
|
||||
dialogVisible: boolean,
|
||||
dialogTitle: string,
|
||||
dialogMessage: string,
|
||||
dialogButtons: Array<OptionsDialogButtonType>,
|
||||
onDialogDismiss: () => void,
|
||||
};
|
||||
|
||||
class GameMainScreen extends React.Component<Props, State> {
|
||||
class GameMainScreen extends React.Component<PropsType, StateType> {
|
||||
static getFormattedTime(seconds: number): string {
|
||||
const date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(seconds);
|
||||
let format;
|
||||
if (date.getHours())
|
||||
format = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
else if (date.getMinutes())
|
||||
format = `${date.getMinutes()}:${date.getSeconds()}`;
|
||||
else format = date.getSeconds().toString();
|
||||
return format;
|
||||
}
|
||||
|
||||
logic: GameLogic;
|
||||
highScore: number | null;
|
||||
logic: GameLogic;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.logic = new GameLogic(20, 10, this.props.theme);
|
||||
this.state = {
|
||||
grid: this.logic.getCurrentGrid(),
|
||||
gameRunning: false,
|
||||
gameTime: 0,
|
||||
gameScore: 0,
|
||||
gameLevel: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "",
|
||||
dialogMessage: "",
|
||||
dialogButtons: [],
|
||||
onDialogDismiss: () => {
|
||||
},
|
||||
};
|
||||
if (this.props.route.params != null)
|
||||
this.highScore = this.props.route.params.highScore;
|
||||
}
|
||||
highScore: number | null;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerRight: this.getRightButton,
|
||||
});
|
||||
this.startGame();
|
||||
}
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.logic = new GameLogic(20, 10, props.theme);
|
||||
this.state = {
|
||||
grid: this.logic.getCurrentGrid(),
|
||||
gameTime: 0,
|
||||
gameScore: 0,
|
||||
gameLevel: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: '',
|
||||
dialogMessage: '',
|
||||
dialogButtons: [],
|
||||
onDialogDismiss: () => {},
|
||||
};
|
||||
if (props.route.params != null)
|
||||
this.highScore = props.route.params.highScore;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.logic.stopGame();
|
||||
}
|
||||
componentDidMount() {
|
||||
const {navigation} = this.props;
|
||||
navigation.setOptions({
|
||||
headerRight: this.getRightButton,
|
||||
});
|
||||
this.startGame();
|
||||
}
|
||||
|
||||
getRightButton = () => {
|
||||
return <MaterialHeaderButtons>
|
||||
<Item title="pause" iconName="pause" onPress={this.togglePause}/>
|
||||
</MaterialHeaderButtons>;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.logic.endGame(false);
|
||||
}
|
||||
|
||||
getFormattedTime(seconds: number) {
|
||||
let date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(seconds);
|
||||
let format;
|
||||
if (date.getHours())
|
||||
format = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
|
||||
else if (date.getMinutes())
|
||||
format = date.getMinutes() + ':' + date.getSeconds();
|
||||
else
|
||||
format = date.getSeconds();
|
||||
return format;
|
||||
}
|
||||
getRightButton = (): React.Node => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item title="pause" iconName="pause" onPress={this.togglePause} />
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
onTick = (score: number, level: number, newGrid: Grid) => {
|
||||
this.setState({
|
||||
gameScore: score,
|
||||
gameLevel: level,
|
||||
grid: newGrid,
|
||||
});
|
||||
}
|
||||
onTick = (score: number, level: number, newGrid: GridType) => {
|
||||
this.setState({
|
||||
gameScore: score,
|
||||
gameLevel: level,
|
||||
grid: newGrid,
|
||||
});
|
||||
};
|
||||
|
||||
onClock = (time: number) => {
|
||||
this.setState({
|
||||
gameTime: time,
|
||||
});
|
||||
}
|
||||
onClock = (time: number) => {
|
||||
this.setState({
|
||||
gameTime: time,
|
||||
});
|
||||
};
|
||||
|
||||
updateGrid = (newGrid: Grid) => {
|
||||
this.setState({
|
||||
grid: newGrid,
|
||||
});
|
||||
}
|
||||
onDialogDismiss = () => {
|
||||
this.setState({dialogVisible: false});
|
||||
};
|
||||
|
||||
updateGridScore = (newGrid: Grid, score: number) => {
|
||||
this.setState({
|
||||
grid: newGrid,
|
||||
gameScore: score,
|
||||
});
|
||||
}
|
||||
onGameEnd = (time: number, score: number, isRestart: boolean) => {
|
||||
const {props, state} = this;
|
||||
this.setState({
|
||||
gameTime: time,
|
||||
gameScore: score,
|
||||
});
|
||||
if (!isRestart)
|
||||
props.navigation.replace('game-start', {
|
||||
score: state.gameScore,
|
||||
level: state.gameLevel,
|
||||
time: state.gameTime,
|
||||
});
|
||||
};
|
||||
|
||||
togglePause = () => {
|
||||
this.logic.togglePause();
|
||||
if (this.logic.isGamePaused())
|
||||
this.showPausePopup();
|
||||
}
|
||||
getStatusIcons(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<Caption
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 5,
|
||||
}}>
|
||||
{i18n.t('screens.game.time')}
|
||||
</Caption>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
name="timer"
|
||||
color={props.theme.colors.subtitle}
|
||||
size={20}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
color: props.theme.colors.subtitle,
|
||||
}}>
|
||||
{GameMainScreen.getFormattedTime(state.gameTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Caption
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 5,
|
||||
}}>
|
||||
{i18n.t('screens.game.level')}
|
||||
</Caption>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
name="gamepad-square"
|
||||
color={props.theme.colors.text}
|
||||
size={20}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
}}>
|
||||
{state.gameLevel}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
onDialogDismiss = () => this.setState({dialogVisible: false});
|
||||
getScoreIcon(): React.Node {
|
||||
const {props, state} = this;
|
||||
const highScore =
|
||||
this.highScore == null || state.gameScore > this.highScore
|
||||
? state.gameScore
|
||||
: this.highScore;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
fontSize: 20,
|
||||
}}>
|
||||
{i18n.t('screens.game.score', {score: state.gameScore})}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="star"
|
||||
color={props.theme.colors.tetrisScore}
|
||||
size={20}
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginLeft: 5,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 5,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
fontSize: 10,
|
||||
color: props.theme.colors.textDisabled,
|
||||
}}>
|
||||
{i18n.t('screens.game.highScore', {score: highScore})}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="star"
|
||||
color={props.theme.colors.tetrisScore}
|
||||
size={10}
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginLeft: 5,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
showPausePopup = () => {
|
||||
const onDismiss = () => {
|
||||
this.togglePause();
|
||||
getControlButtons(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: 80,
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<IconButton
|
||||
icon="rotate-right-variant"
|
||||
size={40}
|
||||
onPress={() => {
|
||||
this.logic.rotatePressed(this.updateGrid);
|
||||
}}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flex: 4,
|
||||
}}>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
size={40}
|
||||
style={{flex: 1}}
|
||||
onPress={() => {
|
||||
this.logic.pressedOut();
|
||||
}}
|
||||
onPressIn={() => {
|
||||
this.logic.leftPressedIn(this.updateGrid);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
size={40}
|
||||
style={{flex: 1}}
|
||||
onPress={() => {
|
||||
this.logic.pressedOut();
|
||||
}}
|
||||
onPressIn={() => {
|
||||
this.logic.rightPressed(this.updateGrid);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<IconButton
|
||||
icon="arrow-down-bold"
|
||||
size={40}
|
||||
onPressIn={() => {
|
||||
this.logic.downPressedIn(this.updateGridScore);
|
||||
}}
|
||||
onPress={() => {
|
||||
this.logic.pressedOut();
|
||||
}}
|
||||
style={{flex: 1}}
|
||||
color={props.theme.colors.tetrisScore}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
updateGrid = (newGrid: GridType) => {
|
||||
this.setState({
|
||||
grid: newGrid,
|
||||
});
|
||||
};
|
||||
|
||||
updateGridScore = (newGrid: GridType, score?: number) => {
|
||||
this.setState((prevState: StateType): {
|
||||
grid: GridType,
|
||||
gameScore: number,
|
||||
} => ({
|
||||
grid: newGrid,
|
||||
gameScore: score != null ? score : prevState.gameScore,
|
||||
}));
|
||||
};
|
||||
|
||||
togglePause = () => {
|
||||
this.logic.togglePause();
|
||||
if (this.logic.isGamePaused()) this.showPausePopup();
|
||||
};
|
||||
|
||||
showPausePopup = () => {
|
||||
const onDismiss = () => {
|
||||
this.togglePause();
|
||||
this.onDialogDismiss();
|
||||
};
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogTitle: i18n.t('screens.game.pause'),
|
||||
dialogMessage: i18n.t('screens.game.pauseMessage'),
|
||||
dialogButtons: [
|
||||
{
|
||||
title: i18n.t('screens.game.restart.text'),
|
||||
onPress: this.showRestartConfirm,
|
||||
},
|
||||
{
|
||||
title: i18n.t('screens.game.resume'),
|
||||
onPress: onDismiss,
|
||||
},
|
||||
],
|
||||
onDialogDismiss: onDismiss,
|
||||
});
|
||||
};
|
||||
|
||||
showRestartConfirm = () => {
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogTitle: i18n.t('screens.game.restart.confirm'),
|
||||
dialogMessage: i18n.t('screens.game.restart.confirmMessage'),
|
||||
dialogButtons: [
|
||||
{
|
||||
title: i18n.t('screens.game.restart.confirmYes'),
|
||||
onPress: () => {
|
||||
this.onDialogDismiss();
|
||||
};
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogTitle: i18n.t("screens.game.pause"),
|
||||
dialogMessage: i18n.t("screens.game.pauseMessage"),
|
||||
dialogButtons: [
|
||||
{
|
||||
title: i18n.t("screens.game.restart.text"),
|
||||
onPress: this.showRestartConfirm
|
||||
},
|
||||
{
|
||||
title: i18n.t("screens.game.resume"),
|
||||
onPress: onDismiss
|
||||
}
|
||||
],
|
||||
onDialogDismiss: onDismiss,
|
||||
});
|
||||
}
|
||||
this.startGame();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.t('screens.game.restart.confirmNo'),
|
||||
onPress: this.showPausePopup,
|
||||
},
|
||||
],
|
||||
onDialogDismiss: this.showPausePopup,
|
||||
});
|
||||
};
|
||||
|
||||
showRestartConfirm = () => {
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
dialogTitle: i18n.t("screens.game.restart.confirm"),
|
||||
dialogMessage: i18n.t("screens.game.restart.confirmMessage"),
|
||||
dialogButtons: [
|
||||
{
|
||||
title: i18n.t("screens.game.restart.confirmYes"),
|
||||
onPress: () => {
|
||||
this.onDialogDismiss();
|
||||
this.startGame();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: i18n.t("screens.game.restart.confirmNo"),
|
||||
onPress: this.showPausePopup
|
||||
}
|
||||
],
|
||||
onDialogDismiss: this.showPausePopup,
|
||||
});
|
||||
}
|
||||
startGame = () => {
|
||||
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
|
||||
};
|
||||
|
||||
startGame = () => {
|
||||
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
|
||||
this.setState({
|
||||
gameRunning: true,
|
||||
});
|
||||
}
|
||||
|
||||
onGameEnd = (time: number, score: number, isRestart: boolean) => {
|
||||
this.setState({
|
||||
gameTime: time,
|
||||
gameScore: score,
|
||||
gameRunning: false,
|
||||
});
|
||||
if (!isRestart)
|
||||
this.props.navigation.replace(
|
||||
"game-start",
|
||||
{
|
||||
score: this.state.gameScore,
|
||||
level: this.state.gameLevel,
|
||||
time: this.state.gameTime,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getStatusIcons() {
|
||||
return (
|
||||
<View style={{
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
{this.getStatusIcons()}
|
||||
<View style={{flex: 4}}>
|
||||
{this.getScoreIcon()}
|
||||
<GridComponent
|
||||
width={this.logic.getWidth()}
|
||||
height={this.logic.getHeight()}
|
||||
grid={state.grid}
|
||||
style={{
|
||||
backgroundColor: props.theme.colors.tetrisBackground,
|
||||
flex: 1,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto"
|
||||
}}>
|
||||
<View style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<Caption style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginBottom: 5,
|
||||
}}>{i18n.t("screens.game.time")}</Caption>
|
||||
<View style={{
|
||||
flexDirection: "row"
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
name={'timer'}
|
||||
color={this.props.theme.colors.subtitle}
|
||||
size={20}/>
|
||||
<Text style={{
|
||||
marginLeft: 5,
|
||||
color: this.props.theme.colors.subtitle
|
||||
}}>{this.getFormattedTime(this.state.gameTime)}</Text>
|
||||
</View>
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
<View style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 20,
|
||||
}}>
|
||||
<Caption style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginBottom: 5,
|
||||
}}>{i18n.t("screens.game.level")}</Caption>
|
||||
<View style={{
|
||||
flexDirection: "row"
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
name={'gamepad-square'}
|
||||
color={this.props.theme.colors.text}
|
||||
size={20}/>
|
||||
<Text style={{
|
||||
marginLeft: 5
|
||||
}}>{this.state.gameLevel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getScoreIcon() {
|
||||
let highScore = this.highScore == null || this.state.gameScore > this.highScore
|
||||
? this.state.gameScore
|
||||
: this.highScore;
|
||||
return (
|
||||
<View style={{
|
||||
<View style={{flex: 1}}>
|
||||
<Preview
|
||||
items={this.logic.getNextPiecesPreviews()}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}>
|
||||
<Text style={{
|
||||
marginLeft: 5,
|
||||
fontSize: 20,
|
||||
}}>{i18n.t("screens.game.score", {score: this.state.gameScore})}</Text>
|
||||
<MaterialCommunityIcons
|
||||
name={'star'}
|
||||
color={this.props.theme.colors.tetrisScore}
|
||||
size={20}
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
marginLeft: 5
|
||||
}}/>
|
||||
</View>
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: 5,
|
||||
}}>
|
||||
<Text style={{
|
||||
marginLeft: 5,
|
||||
fontSize: 10,
|
||||
color: this.props.theme.colors.textDisabled
|
||||
}}>{i18n.t("screens.game.highScore", {score: highScore})}</Text>
|
||||
<MaterialCommunityIcons
|
||||
name={'star'}
|
||||
color={this.props.theme.colors.tetrisScore}
|
||||
size={10}
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
marginLeft: 5
|
||||
}}/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
getControlButtons() {
|
||||
return (
|
||||
<View style={{
|
||||
height: 80,
|
||||
flexDirection: "row"
|
||||
}}>
|
||||
<IconButton
|
||||
icon="rotate-right-variant"
|
||||
size={40}
|
||||
onPress={() => this.logic.rotatePressed(this.updateGrid)}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
flex: 4
|
||||
}}>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
size={40}
|
||||
style={{flex: 1}}
|
||||
onPress={() => this.logic.pressedOut()}
|
||||
onPressIn={() => this.logic.leftPressedIn(this.updateGrid)}
|
||||
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
size={40}
|
||||
style={{flex: 1}}
|
||||
onPress={() => this.logic.pressedOut()}
|
||||
onPressIn={() => this.logic.rightPressed(this.updateGrid)}
|
||||
/>
|
||||
</View>
|
||||
<IconButton
|
||||
icon="arrow-down-bold"
|
||||
size={40}
|
||||
onPressIn={() => this.logic.downPressedIn(this.updateGridScore)}
|
||||
onPress={() => this.logic.pressedOut()}
|
||||
style={{flex: 1}}
|
||||
color={this.props.theme.colors.tetrisScore}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
}}>
|
||||
{this.getStatusIcons()}
|
||||
<View style={{flex: 4}}>
|
||||
{this.getScoreIcon()}
|
||||
<GridComponent
|
||||
width={this.logic.getWidth()}
|
||||
height={this.logic.getHeight()}
|
||||
grid={this.state.grid}
|
||||
style={{
|
||||
backgroundColor: this.props.theme.colors.tetrisBackground,
|
||||
flex: 1,
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{flex: 1}}>
|
||||
<Preview
|
||||
items={this.logic.getNextPiecesPreviews()}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{this.getControlButtons()}
|
||||
|
||||
<OptionsDialog
|
||||
visible={this.state.dialogVisible}
|
||||
title={this.state.dialogTitle}
|
||||
message={this.state.dialogMessage}
|
||||
buttons={this.state.dialogButtons}
|
||||
onDismiss={this.state.onDialogDismiss}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{this.getControlButtons()}
|
||||
|
||||
<OptionsDialog
|
||||
visible={state.dialogVisible}
|
||||
title={state.dialogTitle}
|
||||
message={state.dialogMessage}
|
||||
buttons={state.dialogButtons}
|
||||
onDismiss={state.onDialogDismiss}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(GameMainScreen);
|
||||
|
|
|
|||
|
|
@ -1,427 +1,440 @@
|
|||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper";
|
||||
import {View} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot";
|
||||
import MascotPopup from "../../../components/Mascot/MascotPopup";
|
||||
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
|
||||
import type {Grid} from "../components/GridComponent";
|
||||
import GridComponent from "../components/GridComponent";
|
||||
import GridManager from "../logic/GridManager";
|
||||
import Piece from "../logic/Piece";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
import SpeechArrow from "../../../components/Mascot/SpeechArrow";
|
||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
||||
import * as React from 'react';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Headline,
|
||||
Paragraph,
|
||||
Text,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
||||
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
|
||||
import type {GridType} from '../components/GridComponent';
|
||||
import GridComponent from '../components/GridComponent';
|
||||
import GridManager from '../logic/GridManager';
|
||||
import Piece from '../logic/Piece';
|
||||
import SpeechArrow from '../../../components/Mascot/SpeechArrow';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type GameStats = {
|
||||
score: number,
|
||||
level: number,
|
||||
time: number,
|
||||
}
|
||||
type GameStatsType = {
|
||||
score: number,
|
||||
level: number,
|
||||
time: number,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {
|
||||
params: GameStats
|
||||
},
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {
|
||||
params: GameStatsType,
|
||||
},
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class GameStartScreen extends React.Component<Props> {
|
||||
class GameStartScreen extends React.Component<PropsType> {
|
||||
gridManager: GridManager;
|
||||
|
||||
gridManager: GridManager;
|
||||
scores: Array<number>;
|
||||
scores: Array<number>;
|
||||
|
||||
gameStats: GameStats | null;
|
||||
isHighScore: boolean;
|
||||
gameStats: GameStatsType | null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.gridManager = new GridManager(4, 4, props.theme);
|
||||
this.scores = AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.gameScores.key);
|
||||
this.scores.sort((a, b) => b - a);
|
||||
if (this.props.route.params != null)
|
||||
this.recoverGameScore();
|
||||
isHighScore: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.gridManager = new GridManager(4, 4, props.theme);
|
||||
this.scores = AsyncStorageManager.getObject(
|
||||
AsyncStorageManager.PREFERENCES.gameScores.key,
|
||||
);
|
||||
this.scores.sort((a: number, b: number): number => b - a);
|
||||
if (props.route.params != null) this.recoverGameScore();
|
||||
}
|
||||
|
||||
getPiecesBackground(): React.Node {
|
||||
const {theme} = this.props;
|
||||
const gridList = [];
|
||||
for (let i = 0; i < 18; i += 1) {
|
||||
gridList.push(this.gridManager.getEmptyGrid(4, 4));
|
||||
const piece = new Piece(theme);
|
||||
piece.toGrid(gridList[i], true);
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
{gridList.map((item: GridType, index: number): React.Node => {
|
||||
const size = 10 + Math.floor(Math.random() * 30);
|
||||
const top = Math.floor(Math.random() * 100);
|
||||
const rot = Math.floor(Math.random() * 360);
|
||||
const left = (index % 6) * 20;
|
||||
const animDelay = size * 20;
|
||||
const animDuration = 2 * (2000 - size * 30);
|
||||
return (
|
||||
<Animatable.View
|
||||
animation="fadeInDownBig"
|
||||
delay={animDelay}
|
||||
duration={animDuration}
|
||||
key={`piece${index.toString()}`}
|
||||
style={{
|
||||
width: `${size}%`,
|
||||
position: 'absolute',
|
||||
top: `${top}%`,
|
||||
left: `${left}%`,
|
||||
}}>
|
||||
<GridComponent
|
||||
width={4}
|
||||
height={4}
|
||||
grid={item}
|
||||
style={{
|
||||
transform: [{rotateZ: `${rot}deg`}],
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
recoverGameScore() {
|
||||
this.gameStats = this.props.route.params;
|
||||
this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
|
||||
this.scores.splice(i, 0, this.gameStats.score);
|
||||
break;
|
||||
} else if (this.scores.length <= i) {
|
||||
this.scores.push(this.gameStats.score);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scores.length > 3)
|
||||
this.scores.splice(3, 1);
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameScores.key, this.scores);
|
||||
}
|
||||
|
||||
getPiecesBackground() {
|
||||
let gridList = [];
|
||||
for (let i = 0; i < 18; i++) {
|
||||
gridList.push(this.gridManager.getEmptyGrid(4, 4));
|
||||
const piece = new Piece(this.props.theme);
|
||||
piece.toGrid(gridList[i], true);
|
||||
}
|
||||
return (
|
||||
<View style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}>
|
||||
{gridList.map((item: Grid, index: number) => {
|
||||
const size = 10 + Math.floor(Math.random() * 30);
|
||||
const top = Math.floor(Math.random() * 100);
|
||||
const rot = Math.floor(Math.random() * 360);
|
||||
const left = (index % 6) * 20;
|
||||
const animDelay = size * 20;
|
||||
const animDuration = 2 * (2000 - (size * 30));
|
||||
return (
|
||||
<Animatable.View
|
||||
animation={"fadeInDownBig"}
|
||||
delay={animDelay}
|
||||
duration={animDuration}
|
||||
key={"piece" + index.toString()}
|
||||
style={{
|
||||
width: size + "%",
|
||||
position: "absolute",
|
||||
top: top + "%",
|
||||
left: left + "%",
|
||||
}}
|
||||
>
|
||||
<GridComponent
|
||||
width={4}
|
||||
height={4}
|
||||
grid={item}
|
||||
style={{
|
||||
transform: [{rotateZ: rot + "deg"}],
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getPostGameContent(stats: GameStats) {
|
||||
return (
|
||||
<View style={{
|
||||
flex: 1
|
||||
}}>
|
||||
<Mascot
|
||||
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
|
||||
animated={this.isHighScore}
|
||||
style={{
|
||||
width: this.isHighScore ? "50%" : "30%",
|
||||
marginLeft: this.isHighScore ? "auto" : null,
|
||||
marginRight: this.isHighScore ? "auto" : null,
|
||||
}}/>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.isHighScore ? "60%" : "20%"}}
|
||||
size={20}
|
||||
color={this.props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card style={{
|
||||
borderColor: this.props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 2,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
getPostGameContent(stats: GameStatsType): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
}}>
|
||||
<Mascot
|
||||
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
|
||||
animated={this.isHighScore}
|
||||
style={{
|
||||
width: this.isHighScore ? '50%' : '30%',
|
||||
marginLeft: this.isHighScore ? 'auto' : null,
|
||||
marginRight: this.isHighScore ? 'auto' : null,
|
||||
}}
|
||||
/>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.isHighScore ? '60%' : '20%'}}
|
||||
size={20}
|
||||
color={props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
borderColor: props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 2,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
}}>
|
||||
<Card.Content>
|
||||
<Headline
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: this.isHighScore
|
||||
? props.theme.colors.gameGold
|
||||
: props.theme.colors.primary,
|
||||
}}>
|
||||
{this.isHighScore
|
||||
? i18n.t('screens.game.newHighScore')
|
||||
: i18n.t('screens.game.gameOver')}
|
||||
</Headline>
|
||||
<Divider />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
}}>
|
||||
<Card.Content>
|
||||
<Headline
|
||||
style={{
|
||||
textAlign: "center",
|
||||
color: this.isHighScore
|
||||
? this.props.theme.colors.gameGold
|
||||
: this.props.theme.colors.primary
|
||||
}}>
|
||||
{this.isHighScore
|
||||
? i18n.t("screens.game.newHighScore")
|
||||
: i18n.t("screens.game.gameOver")}
|
||||
</Headline>
|
||||
<Divider/>
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<Text style={{
|
||||
fontSize: 20,
|
||||
}}>
|
||||
{i18n.t("screens.game.score", {score: stats.score})}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name={'star'}
|
||||
color={this.props.theme.colors.tetrisScore}
|
||||
size={30}
|
||||
style={{
|
||||
marginLeft: 5
|
||||
}}/>
|
||||
</View>
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}>
|
||||
<Text>{i18n.t("screens.game.level")}</Text>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
name={"gamepad-square"}
|
||||
size={20}
|
||||
color={this.props.theme.colors.textDisabled}
|
||||
/>
|
||||
<Text>
|
||||
{stats.level}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}>
|
||||
<Text>{i18n.t("screens.game.time")}</Text>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
name={"timer"}
|
||||
size={20}
|
||||
color={this.props.theme.colors.textDisabled}
|
||||
/>
|
||||
<Text>
|
||||
{stats.time}
|
||||
</Text>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
{i18n.t('screens.game.score', {score: stats.score})}
|
||||
</Text>
|
||||
<MaterialCommunityIcons
|
||||
name="star"
|
||||
color={props.theme.colors.tetrisScore}
|
||||
size={30}
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
getWelcomeText() {
|
||||
return (
|
||||
<View>
|
||||
<Mascot emotion={MASCOT_STYLE.COOL} style={{
|
||||
width: "40%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}/>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: "60%"}}
|
||||
size={20}
|
||||
color={this.props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card style={{
|
||||
borderColor: this.props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 2,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}>
|
||||
<Card.Content>
|
||||
<Headline
|
||||
style={{
|
||||
textAlign: "center",
|
||||
color: this.props.theme.colors.primary
|
||||
}}>
|
||||
{i18n.t("screens.game.welcomeTitle")}
|
||||
</Headline>
|
||||
<Divider/>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10,
|
||||
}}>
|
||||
{i18n.t("screens.game.welcomeMessage")}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<Text>{i18n.t('screens.game.level')}</Text>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
name="gamepad-square"
|
||||
size={20}
|
||||
color={props.theme.colors.textDisabled}
|
||||
/>
|
||||
<Text>{stats.level}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<Text>{i18n.t('screens.game.time')}</Text>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
}}
|
||||
name="timer"
|
||||
size={20}
|
||||
color={props.theme.colors.textDisabled}
|
||||
/>
|
||||
<Text>{stats.time}</Text>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getPodiumRender(place: 1 | 2 | 3, score: string) {
|
||||
let icon = "podium-gold";
|
||||
let color = this.props.theme.colors.gameGold;
|
||||
let fontSize = 20;
|
||||
let size = 70;
|
||||
if (place === 2) {
|
||||
icon = "podium-silver";
|
||||
color = this.props.theme.colors.gameSilver;
|
||||
fontSize = 18;
|
||||
size = 60;
|
||||
} else if (place === 3) {
|
||||
icon = "podium-bronze";
|
||||
color = this.props.theme.colors.gameBronze;
|
||||
fontSize = 15;
|
||||
size = 50;
|
||||
}
|
||||
return (
|
||||
<View style={{
|
||||
marginLeft: place === 2 ? 20 : "auto",
|
||||
marginRight: place === 3 ? 20 : "auto",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
getWelcomeText(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View>
|
||||
<Mascot
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
style={{
|
||||
width: '40%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: '60%'}}
|
||||
size={20}
|
||||
color={props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
borderColor: props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 2,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}>
|
||||
<Card.Content>
|
||||
<Headline
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: props.theme.colors.primary,
|
||||
}}>
|
||||
{i18n.t('screens.game.welcomeTitle')}
|
||||
</Headline>
|
||||
<Divider />
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: 10,
|
||||
}}>
|
||||
{i18n.t('screens.game.welcomeMessage')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getPodiumRender(place: 1 | 2 | 3, score: string): React.Node {
|
||||
const {props} = this;
|
||||
let icon = 'podium-gold';
|
||||
let color = props.theme.colors.gameGold;
|
||||
let fontSize = 20;
|
||||
let size = 70;
|
||||
if (place === 2) {
|
||||
icon = 'podium-silver';
|
||||
color = props.theme.colors.gameSilver;
|
||||
fontSize = 18;
|
||||
size = 60;
|
||||
} else if (place === 3) {
|
||||
icon = 'podium-bronze';
|
||||
color = props.theme.colors.gameBronze;
|
||||
fontSize = 15;
|
||||
size = 50;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginLeft: place === 2 ? 20 : 'auto',
|
||||
marginRight: place === 3 ? 20 : 'auto',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}>
|
||||
{this.isHighScore && place === 1 ? (
|
||||
<Animatable.View
|
||||
animation="swing"
|
||||
iterationCount="infinite"
|
||||
duration={2000}
|
||||
delay={1000}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -20,
|
||||
}}>
|
||||
{
|
||||
this.isHighScore && place === 1
|
||||
?
|
||||
<Animatable.View
|
||||
animation={"swing"}
|
||||
iterationCount={"infinite"}
|
||||
duration={2000}
|
||||
delay={1000}
|
||||
useNativeDriver={true}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -20
|
||||
}}
|
||||
>
|
||||
<Animatable.View
|
||||
animation={"pulse"}
|
||||
iterationCount={"infinite"}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={"decagram"}
|
||||
color={this.props.theme.colors.gameGold}
|
||||
size={150}
|
||||
/>
|
||||
</Animatable.View>
|
||||
</Animatable.View>
|
||||
<Animatable.View
|
||||
animation="pulse"
|
||||
iterationCount="infinite"
|
||||
useNativeDriver>
|
||||
<MaterialCommunityIcons
|
||||
name="decagram"
|
||||
color={props.theme.colors.gameGold}
|
||||
size={150}
|
||||
/>
|
||||
</Animatable.View>
|
||||
</Animatable.View>
|
||||
) : null}
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={this.isHighScore && place === 1 ? '#fff' : color}
|
||||
size={size}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontWeight: place === 1 ? 'bold' : null,
|
||||
fontSize,
|
||||
}}>
|
||||
{score}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
: null
|
||||
}
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={this.isHighScore && place === 1 ? "#fff" : color}
|
||||
size={size}
|
||||
/>
|
||||
<Text style={{
|
||||
textAlign: "center",
|
||||
fontWeight: place === 1 ? "bold" : null,
|
||||
fontSize: fontSize,
|
||||
}}>{score}</Text>
|
||||
</View>
|
||||
);
|
||||
getTopScoresRender(): React.Node {
|
||||
const gold = this.scores.length > 0 ? this.scores[0] : '-';
|
||||
const silver = this.scores.length > 1 ? this.scores[1] : '-';
|
||||
const bronze = this.scores.length > 2 ? this.scores[2] : '-';
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
}}>
|
||||
{this.getPodiumRender(1, gold.toString())}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
{this.getPodiumRender(3, bronze.toString())}
|
||||
{this.getPodiumRender(2, silver.toString())}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getMainContent(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{this.gameStats != null
|
||||
? this.getPostGameContent(this.gameStats)
|
||||
: this.getWelcomeText()}
|
||||
<Button
|
||||
icon="play"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
props.navigation.replace('game-main', {
|
||||
highScore: this.scores.length > 0 ? this.scores[0] : null,
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 10,
|
||||
}}>
|
||||
{i18n.t('screens.game.play')}
|
||||
</Button>
|
||||
{this.getTopScoresRender()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
keyExtractor = (item: number): string => item.toString();
|
||||
|
||||
recoverGameScore() {
|
||||
const {route} = this.props;
|
||||
this.gameStats = route.params;
|
||||
this.isHighScore =
|
||||
this.scores.length === 0 || this.gameStats.score > this.scores[0];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
|
||||
this.scores.splice(i, 0, this.gameStats.score);
|
||||
break;
|
||||
} else if (this.scores.length <= i) {
|
||||
this.scores.push(this.gameStats.score);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.scores.length > 3) this.scores.splice(3, 1);
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.gameScores.key,
|
||||
this.scores,
|
||||
);
|
||||
}
|
||||
|
||||
getTopScoresRender() {
|
||||
const gold = this.scores.length > 0
|
||||
? this.scores[0]
|
||||
: "-";
|
||||
const silver = this.scores.length > 1
|
||||
? this.scores[1]
|
||||
: "-";
|
||||
const bronze = this.scores.length > 2
|
||||
? this.scores[2]
|
||||
: "-";
|
||||
return (
|
||||
<View style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20
|
||||
}}>
|
||||
{this.getPodiumRender(1, gold.toString())}
|
||||
<View style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
}}>
|
||||
{this.getPodiumRender(3, bronze.toString())}
|
||||
{this.getPodiumRender(2, silver.toString())}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getMainContent() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{
|
||||
this.gameStats != null
|
||||
? this.getPostGameContent(this.gameStats)
|
||||
: this.getWelcomeText()
|
||||
}
|
||||
<Button
|
||||
icon={"play"}
|
||||
mode={"contained"}
|
||||
onPress={() => this.props.navigation.replace(
|
||||
"game-main",
|
||||
{
|
||||
highScore: this.scores.length > 0
|
||||
? this.scores[0]
|
||||
: null
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
{i18n.t("screens.game.play")}
|
||||
</Button>
|
||||
{this.getTopScoresRender()}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
keyExtractor = (item: number) => item.toString();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{this.getPiecesBackground()}
|
||||
<LinearGradient
|
||||
style={{flex: 1}}
|
||||
colors={[
|
||||
this.props.theme.colors.background + "00",
|
||||
this.props.theme.colors.background
|
||||
]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 0, y: 1}}
|
||||
>
|
||||
<CollapsibleScrollView>
|
||||
{this.getMainContent()}
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key}
|
||||
title={i18n.t("screens.game.mascotDialog.title")}
|
||||
message={i18n.t("screens.game.mascotDialog.message")}
|
||||
icon={"gamepad-variant"}
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t("screens.game.mascotDialog.button"),
|
||||
icon: "check",
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
{this.getPiecesBackground()}
|
||||
<LinearGradient
|
||||
style={{flex: 1}}
|
||||
colors={[
|
||||
`${props.theme.colors.background}00`,
|
||||
props.theme.colors.background,
|
||||
]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 0, y: 1}}>
|
||||
<CollapsibleScrollView>
|
||||
{this.getMainContent()}
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key}
|
||||
title={i18n.t('screens.game.mascotDialog.title')}
|
||||
message={i18n.t('screens.game.mascotDialog.message')}
|
||||
icon="gamepad-variant"
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t('screens.game.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(GameStartScreen);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import MaterialHeaderButtons, {
|
|||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
|
|
@ -78,7 +78,7 @@ type RawDashboardType = {
|
|||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {nextScreen: string, data: {...}}},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
|
|
|
|||
|
|
@ -1,116 +1,139 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Button, Card, Paragraph, withTheme} from "react-native-paper";
|
||||
import i18n from "i18n-js";
|
||||
import {Linking} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
||||
import {Avatar, Button, Card, Paragraph, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import {Linking} from 'react-native';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type Props = {
|
||||
theme: CustomTheme
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const links = {
|
||||
bugsMail: `mailto:app@amicale-insat.fr?subject=[BUG] Application CAMPUS
|
||||
bugsMail: `mailto:app@amicale-insat.fr?subject=[BUG] Application CAMPUS
|
||||
&body=Coucou Arnaud ça bug c'est nul,\n\n
|
||||
Informations sur ton système si tu sais (iOS ou Android, modèle du tel, version):\n\n\n
|
||||
Nature du problème :\n\n\n
|
||||
Étapes pour reproduire ce pb :\n\n\n\n
|
||||
Stp corrige le pb, bien cordialement.`,
|
||||
bugsGit: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new',
|
||||
facebook: "https://www.facebook.com/campus.insat",
|
||||
feedbackMail: `mailto:app@amicale-insat.fr?subject=[FEEDBACK] Application CAMPUS
|
||||
bugsGit:
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new',
|
||||
facebook: 'https://www.facebook.com/campus.insat',
|
||||
feedbackMail: `mailto:app@amicale-insat.fr?subject=[FEEDBACK] Application CAMPUS
|
||||
&body=Coucou Arnaud j'ai du feedback\n\n\n\nBien cordialement.`,
|
||||
feedbackGit: "https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new",
|
||||
}
|
||||
feedbackGit:
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new',
|
||||
};
|
||||
|
||||
class FeedbackScreen extends React.Component<Props> {
|
||||
class FeedbackScreen extends React.Component<PropsType> {
|
||||
/**
|
||||
* Gets link buttons
|
||||
*
|
||||
* @param isBug True if buttons should redirect to bug report methods
|
||||
* @returns {*}
|
||||
*/
|
||||
static getButtons(isBug: boolean): React.Node {
|
||||
return (
|
||||
<Card.Actions
|
||||
style={{
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Button
|
||||
icon="email"
|
||||
mode="contained"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(isBug ? links.bugsMail : links.feedbackMail);
|
||||
}}>
|
||||
MAIL
|
||||
</Button>
|
||||
<Button
|
||||
icon="git"
|
||||
mode="contained"
|
||||
color="#609927"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(isBug ? links.bugsGit : links.feedbackGit);
|
||||
}}>
|
||||
GITEA
|
||||
</Button>
|
||||
<Button
|
||||
icon="facebook"
|
||||
mode="contained"
|
||||
color="#2e88fe"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(links.facebook);
|
||||
}}>
|
||||
Facebook
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets link buttons
|
||||
*
|
||||
* @param isBug True if buttons should redirect to bug report methods
|
||||
* @returns {*}
|
||||
*/
|
||||
getButtons(isBug: boolean) {
|
||||
return (
|
||||
<Card.Actions style={{
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Button
|
||||
icon="email"
|
||||
mode={"contained"}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => Linking.openURL(isBug ? links.bugsMail : links.feedbackMail)}>
|
||||
MAIL
|
||||
</Button>
|
||||
<Button
|
||||
icon="git"
|
||||
mode={"contained"}
|
||||
color={"#609927"}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => Linking.openURL(isBug ? links.bugsGit : links.feedbackGit)}>
|
||||
GITEA
|
||||
</Button>
|
||||
<Button
|
||||
icon="facebook"
|
||||
mode={"contained"}
|
||||
color={"#2e88fe"}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginTop: 5,
|
||||
}}
|
||||
onPress={() => Linking.openURL(links.facebook)}>
|
||||
Facebook
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<CollapsibleScrollView style={{padding: 5}}>
|
||||
<Card>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.feedback.bugs')}
|
||||
subtitle={i18n.t('screens.feedback.bugsSubtitle')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: number,
|
||||
}): React.Node => (
|
||||
<Avatar.Icon size={size} color={color} icon="bug" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>{i18n.t('screens.feedback.bugsDescription')}</Paragraph>
|
||||
<Paragraph style={{color: theme.colors.primary}}>
|
||||
{i18n.t('screens.feedback.contactMeans')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
{FeedbackScreen.getButtons(true)}
|
||||
</Card>
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsibleScrollView style={{padding: 5}}>
|
||||
<Card>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.feedback.bugs')}
|
||||
subtitle={i18n.t('screens.feedback.bugsSubtitle')}
|
||||
left={(props) => <Avatar.Icon {...props} icon="bug"/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('screens.feedback.bugsDescription')}
|
||||
</Paragraph>
|
||||
<Paragraph style={{color: this.props.theme.colors.primary}}>
|
||||
{i18n.t('screens.feedback.contactMeans')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
{this.getButtons(true)}
|
||||
</Card>
|
||||
|
||||
<Card style={{marginTop: 20, marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.feedback.title')}
|
||||
subtitle={i18n.t('screens.feedback.feedbackSubtitle')}
|
||||
left={(props) => <Avatar.Icon {...props} icon="comment"/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('screens.feedback.feedbackDescription')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
{this.getButtons(false)}
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
<Card style={{marginTop: 20, marginBottom: 10}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.feedback.title')}
|
||||
subtitle={i18n.t('screens.feedback.feedbackSubtitle')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: number,
|
||||
}): React.Node => (
|
||||
<Avatar.Icon size={size} color={color} icon="comment" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('screens.feedback.feedbackDescription')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
{FeedbackScreen.getButtons(false)}
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(FeedbackScreen);
|
||||
|
|
|
|||
|
|
@ -1,267 +1,323 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import ThemeManager from '../../../managers/ThemeManager';
|
||||
import i18n from "i18n-js";
|
||||
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Card, List, Switch, ToggleButton, withTheme} from 'react-native-paper';
|
||||
import {Appearance} from "react-native-appearance";
|
||||
import CustomSlider from "../../../components/Overrides/CustomSlider";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
||||
import {Appearance} from 'react-native-appearance';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import ThemeManager from '../../../managers/ThemeManager';
|
||||
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
|
||||
import CustomSlider from '../../../components/Overrides/CustomSlider';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type State = {
|
||||
nightMode: boolean,
|
||||
nightModeFollowSystem: boolean,
|
||||
notificationReminderSelected: number,
|
||||
startScreenPickerSelected: string,
|
||||
isDebugUnlocked: boolean,
|
||||
type StateType = {
|
||||
nightMode: boolean,
|
||||
nightModeFollowSystem: boolean,
|
||||
startScreenPickerSelected: string,
|
||||
isDebugUnlocked: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Class defining the Settings screen. This screen shows controls to modify app preferences.
|
||||
*/
|
||||
class SettingsScreen extends React.Component<Props, State> {
|
||||
class SettingsScreen extends React.Component<PropsType, StateType> {
|
||||
savedNotificationReminder: number;
|
||||
|
||||
savedNotificationReminder: number;
|
||||
/**
|
||||
* Loads user preferences into state
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
const notifReminder = AsyncStorageManager.getString(
|
||||
AsyncStorageManager.PREFERENCES.proxiwashNotifications.key,
|
||||
);
|
||||
this.savedNotificationReminder = parseInt(notifReminder, 10);
|
||||
if (Number.isNaN(this.savedNotificationReminder))
|
||||
this.savedNotificationReminder = 0;
|
||||
|
||||
/**
|
||||
* Loads user preferences into state
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
let notifReminder = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.proxiwashNotifications.key);
|
||||
this.savedNotificationReminder = parseInt(notifReminder);
|
||||
if (isNaN(this.savedNotificationReminder))
|
||||
this.savedNotificationReminder = 0;
|
||||
|
||||
this.state = {
|
||||
nightMode: ThemeManager.getNightMode(),
|
||||
nightModeFollowSystem: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
|
||||
&& Appearance.getColorScheme() !== 'no-preference',
|
||||
notificationReminderSelected: this.savedNotificationReminder,
|
||||
startScreenPickerSelected: AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key),
|
||||
isDebugUnlocked: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.debugUnlocked.key)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks debug mode and saves its state to user preferences
|
||||
*/
|
||||
unlockDebugMode = () => {
|
||||
this.setState({isDebugUnlocked: true});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.debugUnlocked.key, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value for the proxiwash reminder notification time
|
||||
*
|
||||
* @param value The value to store
|
||||
*/
|
||||
onProxiwashNotifPickerValueChange = (value: number) => {
|
||||
this.setState({notificationReminderSelected: value});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.proxiwashNotifications.key, value);
|
||||
this.state = {
|
||||
nightMode: ThemeManager.getNightMode(),
|
||||
nightModeFollowSystem:
|
||||
AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||
) && Appearance.getColorScheme() !== 'no-preference',
|
||||
startScreenPickerSelected: AsyncStorageManager.getString(
|
||||
AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
|
||||
),
|
||||
isDebugUnlocked: AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.debugUnlocked.key,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the value for the proxiwash reminder notification time
|
||||
*
|
||||
* @param value The value to store
|
||||
*/
|
||||
onStartScreenPickerValueChange = (value: string) => {
|
||||
if (value != null) {
|
||||
this.setState({startScreenPickerSelected: value});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.defaultStartScreen.key, value);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Saves the value for the proxiwash reminder notification time
|
||||
*
|
||||
* @param value The value to store
|
||||
*/
|
||||
onProxiwashNotifPickerValueChange = (value: number) => {
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.proxiwashNotifications.key,
|
||||
value,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a picker allowing the user to select the proxiwash reminder notification time
|
||||
*
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getProxiwashNotifPicker() {
|
||||
return (
|
||||
<CustomSlider
|
||||
style={{flex: 1, marginHorizontal: 10, height: 50}}
|
||||
minimumValue={0}
|
||||
maximumValue={10}
|
||||
step={1}
|
||||
value={this.savedNotificationReminder}
|
||||
onValueChange={this.onProxiwashNotifPickerValueChange}
|
||||
thumbTintColor={this.props.theme.colors.primary}
|
||||
minimumTrackTintColor={this.props.theme.colors.primary}
|
||||
/>
|
||||
);
|
||||
/**
|
||||
* Saves the value for the proxiwash reminder notification time
|
||||
*
|
||||
* @param value The value to store
|
||||
*/
|
||||
onStartScreenPickerValueChange = (value: string) => {
|
||||
if (value != null) {
|
||||
this.setState({startScreenPickerSelected: value});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
|
||||
value,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a picker allowing the user to select the start screen
|
||||
*
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getStartScreenPicker() {
|
||||
return (
|
||||
<ToggleButton.Row
|
||||
onValueChange={this.onStartScreenPickerValueChange}
|
||||
value={this.state.startScreenPickerSelected}
|
||||
style={{marginLeft: 'auto', marginRight: 'auto'}}
|
||||
>
|
||||
<ToggleButton icon="account-circle" value="services"/>
|
||||
<ToggleButton icon="tshirt-crew" value="proxiwash"/>
|
||||
<ToggleButton icon="triangle" value="home"/>
|
||||
<ToggleButton icon="calendar-range" value="planning"/>
|
||||
<ToggleButton icon="clock" value="planex"/>
|
||||
</ToggleButton.Row>
|
||||
);
|
||||
/**
|
||||
* Returns a picker allowing the user to select the proxiwash reminder notification time
|
||||
*
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getProxiwashNotifPicker(): React.Node {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<CustomSlider
|
||||
style={{flex: 1, marginHorizontal: 10, height: 50}}
|
||||
minimumValue={0}
|
||||
maximumValue={10}
|
||||
step={1}
|
||||
value={this.savedNotificationReminder}
|
||||
onValueChange={this.onProxiwashNotifPickerValueChange}
|
||||
thumbTintColor={theme.colors.primary}
|
||||
minimumTrackTintColor={theme.colors.primary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a picker allowing the user to select the start screen
|
||||
*
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getStartScreenPicker(): React.Node {
|
||||
const {startScreenPickerSelected} = this.state;
|
||||
return (
|
||||
<ToggleButton.Row
|
||||
onValueChange={this.onStartScreenPickerValueChange}
|
||||
value={startScreenPickerSelected}
|
||||
style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ToggleButton icon="account-circle" value="services" />
|
||||
<ToggleButton icon="tshirt-crew" value="proxiwash" />
|
||||
<ToggleButton icon="triangle" value="home" />
|
||||
<ToggleButton icon="calendar-range" value="planning" />
|
||||
<ToggleButton icon="clock" value="planex" />
|
||||
</ToggleButton.Row>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles night mode and saves it to preferences
|
||||
*/
|
||||
onToggleNightMode = () => {
|
||||
const {nightMode} = this.state;
|
||||
ThemeManager.getInstance().setNightMode(!nightMode);
|
||||
this.setState({nightMode: !nightMode});
|
||||
};
|
||||
|
||||
onToggleNightModeFollowSystem = () => {
|
||||
const {nightModeFollowSystem} = this.state;
|
||||
const value = !nightModeFollowSystem;
|
||||
this.setState({nightModeFollowSystem: value});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||
value,
|
||||
);
|
||||
if (value) {
|
||||
const nightMode = Appearance.getColorScheme() === 'dark';
|
||||
ThemeManager.getInstance().setNightMode(nightMode);
|
||||
this.setState({nightMode});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles night mode and saves it to preferences
|
||||
*/
|
||||
onToggleNightMode = () => {
|
||||
ThemeManager.getInstance().setNightMode(!this.state.nightMode);
|
||||
this.setState({nightMode: !this.state.nightMode});
|
||||
};
|
||||
/**
|
||||
* Gets a list item using a checkbox control
|
||||
*
|
||||
* @param onPressCallback The callback when the checkbox state changes
|
||||
* @param icon The icon name to display on the list item
|
||||
* @param title The text to display as this list item title
|
||||
* @param subtitle The text to display as this list item subtitle
|
||||
* @param state The current state of the switch
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
static getToggleItem(
|
||||
onPressCallback: () => void,
|
||||
icon: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
state: boolean,
|
||||
): React.Node {
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={({size, color}: {size: number, color: number}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon={icon} />
|
||||
)}
|
||||
right={(): React.Node => (
|
||||
<Switch value={state} onValueChange={onPressCallback} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onToggleNightModeFollowSystem = () => {
|
||||
const value = !this.state.nightModeFollowSystem;
|
||||
this.setState({nightModeFollowSystem: value});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key, value);
|
||||
if (value) {
|
||||
const nightMode = Appearance.getColorScheme() === 'dark';
|
||||
ThemeManager.getInstance().setNightMode(nightMode);
|
||||
this.setState({nightMode: nightMode});
|
||||
}
|
||||
};
|
||||
getNavigateItem(
|
||||
route: string,
|
||||
icon: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
onLongPress?: () => void,
|
||||
): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
onPress={() => {
|
||||
navigation.navigate(route);
|
||||
}}
|
||||
left={({size, color}: {size: number, color: number}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon={icon} />
|
||||
)}
|
||||
right={({size, color}: {size: number, color: number}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="chevron-right" />
|
||||
)}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list item using a checkbox control
|
||||
*
|
||||
* @param onPressCallback The callback when the checkbox state changes
|
||||
* @param icon The icon name to display on the list item
|
||||
* @param title The text to display as this list item title
|
||||
* @param subtitle The text to display as this list item subtitle
|
||||
* @param state The current state of the switch
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getToggleItem(onPressCallback: Function, icon: string, title: string, subtitle: string, state: boolean) {
|
||||
return (
|
||||
/**
|
||||
* Unlocks debug mode and saves its state to user preferences
|
||||
*/
|
||||
unlockDebugMode = () => {
|
||||
this.setState({isDebugUnlocked: true});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.debugUnlocked.key,
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {nightModeFollowSystem, nightMode, isDebugUnlocked} = this.state;
|
||||
return (
|
||||
<CollapsibleScrollView>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title={i18n.t('screens.settings.generalCard')} />
|
||||
<List.Section>
|
||||
{Appearance.getColorScheme() !== 'no-preference'
|
||||
? SettingsScreen.getToggleItem(
|
||||
this.onToggleNightModeFollowSystem,
|
||||
'theme-light-dark',
|
||||
i18n.t('screens.settings.nightModeAuto'),
|
||||
i18n.t('screens.settings.nightModeAutoSub'),
|
||||
nightModeFollowSystem,
|
||||
)
|
||||
: null}
|
||||
{Appearance.getColorScheme() === 'no-preference' ||
|
||||
!nightModeFollowSystem
|
||||
? SettingsScreen.getToggleItem(
|
||||
this.onToggleNightMode,
|
||||
'theme-light-dark',
|
||||
i18n.t('screens.settings.nightMode'),
|
||||
nightMode
|
||||
? i18n.t('screens.settings.nightModeSubOn')
|
||||
: i18n.t('screens.settings.nightModeSubOff'),
|
||||
nightMode,
|
||||
)
|
||||
: null}
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={props => <List.Icon {...props} icon={icon}/>}
|
||||
right={() =>
|
||||
<Switch
|
||||
value={state}
|
||||
onValueChange={onPressCallback}
|
||||
/>}
|
||||
title={i18n.t('screens.settings.startScreen')}
|
||||
description={i18n.t('screens.settings.startScreenSub')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: number,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="power" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getNavigateItem(route: string, icon: string, title: string, subtitle: string, onLongPress?: () => void) {
|
||||
return (
|
||||
{this.getStartScreenPicker()}
|
||||
{this.getNavigateItem(
|
||||
'dashboard-edit',
|
||||
'view-dashboard',
|
||||
i18n.t('screens.settings.dashboard'),
|
||||
i18n.t('screens.settings.dashboardSub'),
|
||||
)}
|
||||
</List.Section>
|
||||
</Card>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title="Proxiwash" />
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
onPress={() => this.props.navigation.navigate(route)}
|
||||
left={props => <List.Icon {...props} icon={icon}/>}
|
||||
right={props => <List.Icon {...props} icon={"chevron-right"}/>}
|
||||
onLongPress={onLongPress}
|
||||
title={i18n.t('screens.settings.proxiwashNotifReminder')}
|
||||
description={i18n.t('screens.settings.proxiwashNotifReminderSub')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: number,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="washing-machine" />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsibleScrollView>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title={i18n.t('screens.settings.generalCard')}/>
|
||||
<List.Section>
|
||||
{Appearance.getColorScheme() !== 'no-preference' ? this.getToggleItem(
|
||||
this.onToggleNightModeFollowSystem,
|
||||
'theme-light-dark',
|
||||
i18n.t('screens.settings.nightModeAuto'),
|
||||
i18n.t('screens.settings.nightModeAutoSub'),
|
||||
this.state.nightModeFollowSystem
|
||||
) : null}
|
||||
{
|
||||
Appearance.getColorScheme() === 'no-preference' || !this.state.nightModeFollowSystem ?
|
||||
this.getToggleItem(
|
||||
this.onToggleNightMode,
|
||||
'theme-light-dark',
|
||||
i18n.t('screens.settings.nightMode'),
|
||||
this.state.nightMode ?
|
||||
i18n.t('screens.settings.nightModeSubOn') :
|
||||
i18n.t('screens.settings.nightModeSubOff'),
|
||||
this.state.nightMode
|
||||
) : null
|
||||
}
|
||||
<List.Item
|
||||
title={i18n.t('screens.settings.startScreen')}
|
||||
description={i18n.t('screens.settings.startScreenSub')}
|
||||
left={props => <List.Icon {...props} icon="power"/>}
|
||||
/>
|
||||
{this.getStartScreenPicker()}
|
||||
{this.getNavigateItem(
|
||||
"dashboard-edit",
|
||||
"view-dashboard",
|
||||
i18n.t('screens.settings.dashboard'),
|
||||
i18n.t('screens.settings.dashboardSub')
|
||||
)}
|
||||
</List.Section>
|
||||
</Card>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title="Proxiwash"/>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title={i18n.t('screens.settings.proxiwashNotifReminder')}
|
||||
description={i18n.t('screens.settings.proxiwashNotifReminderSub')}
|
||||
left={props => <List.Icon {...props} icon="washing-machine"/>}
|
||||
opened={true}
|
||||
/>
|
||||
<View style={{marginLeft: 30}}>
|
||||
{this.getProxiwashNotifPicker()}
|
||||
</View>
|
||||
</List.Section>
|
||||
</Card>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title={i18n.t('screens.settings.information')}/>
|
||||
<List.Section>
|
||||
{this.state.isDebugUnlocked
|
||||
? this.getNavigateItem(
|
||||
"debug",
|
||||
"bug-check",
|
||||
i18n.t('screens.debug.title'),
|
||||
""
|
||||
)
|
||||
: null}
|
||||
{this.getNavigateItem(
|
||||
"about",
|
||||
"information",
|
||||
i18n.t('screens.about.title'),
|
||||
i18n.t('screens.about.buttonDesc'),
|
||||
this.unlockDebugMode,
|
||||
)}
|
||||
{this.getNavigateItem(
|
||||
"feedback",
|
||||
"comment-quote",
|
||||
i18n.t('screens.feedback.homeButtonTitle'),
|
||||
i18n.t('screens.feedback.homeButtonSubtitle'),
|
||||
)}
|
||||
</List.Section>
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
<View style={{marginLeft: 30}}>
|
||||
{this.getProxiwashNotifPicker()}
|
||||
</View>
|
||||
</List.Section>
|
||||
</Card>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title title={i18n.t('screens.settings.information')} />
|
||||
<List.Section>
|
||||
{isDebugUnlocked
|
||||
? this.getNavigateItem(
|
||||
'debug',
|
||||
'bug-check',
|
||||
i18n.t('screens.debug.title'),
|
||||
'',
|
||||
)
|
||||
: null}
|
||||
{this.getNavigateItem(
|
||||
'about',
|
||||
'information',
|
||||
i18n.t('screens.about.title'),
|
||||
i18n.t('screens.about.buttonDesc'),
|
||||
this.unlockDebugMode,
|
||||
)}
|
||||
{this.getNavigateItem(
|
||||
'feedback',
|
||||
'comment-quote',
|
||||
i18n.t('screens.feedback.homeButtonTitle'),
|
||||
i18n.t('screens.feedback.homeButtonSubtitle'),
|
||||
)}
|
||||
</List.Section>
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SettingsScreen);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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 type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import ThemeManager from '../../managers/ThemeManager';
|
||||
import WebViewScreen from '../../components/Screens/WebViewScreen';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
|
|
@ -22,7 +22,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
|
|||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {group: PlanexGroupType}},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
|
|
|
|||
|
|
@ -2,166 +2,183 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
|
||||
import {Card, withTheme} from 'react-native-paper';
|
||||
import DateManager from "../../managers/DateManager";
|
||||
import ImageModal from 'react-native-image-modal';
|
||||
import BasicLoadingScreen from "../../components/Screens/BasicLoadingScreen";
|
||||
import {apiRequest, ERROR_TYPE} from "../../utils/WebData";
|
||||
import ErrorView from "../../components/Screens/ErrorView";
|
||||
import CustomHTML from "../../components/Overrides/CustomHTML";
|
||||
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
|
||||
import DateManager from '../../managers/DateManager';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
import {apiRequest, ERROR_TYPE} from '../../utils/WebData';
|
||||
import ErrorView from '../../components/Screens/ErrorView';
|
||||
import CustomHTML from '../../components/Overrides/CustomHTML';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
import type {PlanningEventType} from '../../utils/Planning';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: { params: { data: Object, id: number, eventId: number } },
|
||||
theme: CustomTheme
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {data: PlanningEventType, id: number, eventId: number}},
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean
|
||||
type StateType = {
|
||||
loading: boolean,
|
||||
};
|
||||
|
||||
const CLUB_INFO_PATH = "event/info";
|
||||
const EVENT_INFO_URL = 'event/info';
|
||||
|
||||
/**
|
||||
* Class defining a planning event information page.
|
||||
*/
|
||||
class PlanningDisplayScreen extends React.Component<Props, State> {
|
||||
class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
||||
displayData: null | PlanningEventType;
|
||||
|
||||
displayData: Object;
|
||||
shouldFetchData: boolean;
|
||||
eventId: number;
|
||||
errorCode: number;
|
||||
shouldFetchData: boolean;
|
||||
|
||||
/**
|
||||
* Generates data depending on whether the screen was opened from the planning or from a link
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
eventId: number;
|
||||
|
||||
if (this.props.route.params.data != null) {
|
||||
this.displayData = this.props.route.params.data;
|
||||
this.eventId = this.displayData.id;
|
||||
this.shouldFetchData = false;
|
||||
this.errorCode = 0;
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
} else {
|
||||
this.displayData = null;
|
||||
this.eventId = this.props.route.params.eventId;
|
||||
this.shouldFetchData = true;
|
||||
this.errorCode = 0;
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
this.fetchData();
|
||||
errorCode: number;
|
||||
|
||||
}
|
||||
/**
|
||||
* Generates data depending on whether the screen was opened from the planning or from a link
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
if (props.route.params.data != null) {
|
||||
this.displayData = props.route.params.data;
|
||||
this.eventId = this.displayData.id;
|
||||
this.shouldFetchData = false;
|
||||
this.errorCode = 0;
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
} else {
|
||||
this.displayData = null;
|
||||
this.eventId = props.route.params.eventId;
|
||||
this.shouldFetchData = true;
|
||||
this.errorCode = 0;
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for the current event id from the API
|
||||
*/
|
||||
fetchData = () => {
|
||||
this.setState({loading: true});
|
||||
apiRequest(CLUB_INFO_PATH, 'POST', {id: this.eventId})
|
||||
.then(this.onFetchSuccess)
|
||||
.catch(this.onFetchError);
|
||||
};
|
||||
/**
|
||||
* Hides loading and saves fetched data
|
||||
*
|
||||
* @param data Received data
|
||||
*/
|
||||
onFetchSuccess = (data: PlanningEventType) => {
|
||||
this.displayData = data;
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides loading and saves fetched data
|
||||
*
|
||||
* @param data Received data
|
||||
*/
|
||||
onFetchSuccess = (data: Object) => {
|
||||
this.displayData = data;
|
||||
this.setState({loading: false});
|
||||
};
|
||||
/**
|
||||
* Hides loading and saves the error code
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
onFetchError = (error: number) => {
|
||||
this.errorCode = error;
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides loading and saves the error code
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
onFetchError = (error: number) => {
|
||||
this.errorCode = error;
|
||||
this.setState({loading: false});
|
||||
};
|
||||
/**
|
||||
* Gets content to display
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getContent(): React.Node {
|
||||
const {theme} = this.props;
|
||||
const {displayData} = this;
|
||||
if (displayData == null) return null;
|
||||
let subtitle = getFormattedEventTime(
|
||||
displayData.date_begin,
|
||||
displayData.date_end,
|
||||
);
|
||||
const dateString = getDateOnlyString(displayData.date_begin);
|
||||
if (dateString !== null)
|
||||
subtitle += ` | ${DateManager.getInstance().getTranslatedDate(
|
||||
dateString,
|
||||
)}`;
|
||||
return (
|
||||
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
|
||||
<Card.Title title={displayData.title} subtitle={subtitle} />
|
||||
{displayData.logo !== null ? (
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor={theme.colors.background}
|
||||
style={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
}}
|
||||
source={{
|
||||
uri: displayData.logo,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
/**
|
||||
* Gets content to display
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getContent() {
|
||||
let subtitle = getFormattedEventTime(
|
||||
this.displayData["date_begin"], this.displayData["date_end"]);
|
||||
let dateString = getDateOnlyString(this.displayData["date_begin"]);
|
||||
if (dateString !== null)
|
||||
subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString);
|
||||
return (
|
||||
<CollapsibleScrollView
|
||||
style={{paddingLeft: 5, paddingRight: 5}}
|
||||
hasTab={true}
|
||||
>
|
||||
<Card.Title
|
||||
title={this.displayData.title}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
{this.displayData.logo !== null ?
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor={this.props.theme.colors.background}
|
||||
style={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
}}
|
||||
source={{
|
||||
uri: this.displayData.logo,
|
||||
}}
|
||||
/></View>
|
||||
: <View/>}
|
||||
{displayData.description !== null ? (
|
||||
<Card.Content
|
||||
style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
<CustomHTML html={displayData.description} />
|
||||
</Card.Content>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
{this.displayData.description !== null ?
|
||||
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
<CustomHTML html={this.displayData.description}/>
|
||||
</Card.Content>
|
||||
: <View/>}
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Shows an error view and use a custom message if the event does not exist
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getErrorView(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
if (this.errorCode === ERROR_TYPE.BAD_INPUT)
|
||||
return (
|
||||
<ErrorView
|
||||
navigation={navigation}
|
||||
showRetryButton={false}
|
||||
message={i18n.t('screens.planning.invalidEvent')}
|
||||
icon="calendar-remove"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ErrorView
|
||||
navigation={navigation}
|
||||
errorCode={this.errorCode}
|
||||
onRefresh={this.fetchData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error view and use a custom message if the event does not exist
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getErrorView() {
|
||||
if (this.errorCode === ERROR_TYPE.BAD_INPUT)
|
||||
return <ErrorView {...this.props} showRetryButton={false} message={i18n.t("screens.planning.invalidEvent")}
|
||||
icon={"calendar-remove"}/>;
|
||||
else
|
||||
return <ErrorView {...this.props} errorCode={this.errorCode} onRefresh={this.fetchData}/>;
|
||||
}
|
||||
/**
|
||||
* Fetches data for the current event id from the API
|
||||
*/
|
||||
fetchData = () => {
|
||||
this.setState({loading: true});
|
||||
apiRequest(EVENT_INFO_URL, 'POST', {id: this.eventId})
|
||||
.then(this.onFetchSuccess)
|
||||
.catch(this.onFetchError);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.loading)
|
||||
return <BasicLoadingScreen/>;
|
||||
else if (this.errorCode === 0)
|
||||
return this.getContent();
|
||||
else
|
||||
return this.getErrorView();
|
||||
}
|
||||
render(): React.Node {
|
||||
const {loading} = this.state;
|
||||
if (loading) return <BasicLoadingScreen />;
|
||||
if (this.errorCode === 0) return this.getContent();
|
||||
return this.getErrorView();
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(PlanningDisplayScreen);
|
||||
|
|
|
|||
|
|
@ -2,259 +2,282 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {BackHandler, View} from 'react-native';
|
||||
import i18n from "i18n-js";
|
||||
import {LocaleConfig} from 'react-native-calendars';
|
||||
import {readData} from "../../utils/WebData";
|
||||
import type {eventObject} from "../../utils/Planning";
|
||||
import {
|
||||
generateEventAgenda,
|
||||
getCurrentDateString,
|
||||
getDateOnlyString,
|
||||
getFormattedEventTime,
|
||||
} from '../../utils/Planning';
|
||||
import i18n from 'i18n-js';
|
||||
import {Agenda, LocaleConfig} from 'react-native-calendars';
|
||||
import {Avatar, Divider, List} from 'react-native-paper';
|
||||
import CustomAgenda from "../../components/Overrides/CustomAgenda";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {readData} from '../../utils/WebData';
|
||||
import type {PlanningEventType} from '../../utils/Planning';
|
||||
import {
|
||||
generateEventAgenda,
|
||||
getCurrentDateString,
|
||||
getDateOnlyString,
|
||||
getFormattedEventTime,
|
||||
} from '../../utils/Planning';
|
||||
import CustomAgenda from '../../components/Overrides/CustomAgenda';
|
||||
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
|
||||
LocaleConfig.locales['fr'] = {
|
||||
monthNames: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
|
||||
monthNamesShort: ['Janv.', 'Févr.', 'Mars', 'Avril', 'Mai', 'Juin', 'Juil.', 'Août', 'Sept.', 'Oct.', 'Nov.', 'Déc.'],
|
||||
dayNames: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'],
|
||||
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
|
||||
today: 'Aujourd\'hui'
|
||||
LocaleConfig.locales.fr = {
|
||||
monthNames: [
|
||||
'Janvier',
|
||||
'Février',
|
||||
'Mars',
|
||||
'Avril',
|
||||
'Mai',
|
||||
'Juin',
|
||||
'Juillet',
|
||||
'Août',
|
||||
'Septembre',
|
||||
'Octobre',
|
||||
'Novembre',
|
||||
'Décembre',
|
||||
],
|
||||
monthNamesShort: [
|
||||
'Janv.',
|
||||
'Févr.',
|
||||
'Mars',
|
||||
'Avril',
|
||||
'Mai',
|
||||
'Juin',
|
||||
'Juil.',
|
||||
'Août',
|
||||
'Sept.',
|
||||
'Oct.',
|
||||
'Nov.',
|
||||
'Déc.',
|
||||
],
|
||||
dayNames: [
|
||||
'Dimanche',
|
||||
'Lundi',
|
||||
'Mardi',
|
||||
'Mercredi',
|
||||
'Jeudi',
|
||||
'Vendredi',
|
||||
'Samedi',
|
||||
],
|
||||
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
|
||||
today: "Aujourd'hui",
|
||||
};
|
||||
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
}
|
||||
|
||||
type State = {
|
||||
refreshing: boolean,
|
||||
agendaItems: Object,
|
||||
calendarShowing: boolean,
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
};
|
||||
|
||||
const FETCH_URL = "https://www.amicale-insat.fr/api/event/list";
|
||||
type StateType = {
|
||||
refreshing: boolean,
|
||||
agendaItems: {[key: string]: Array<PlanningEventType>},
|
||||
calendarShowing: boolean,
|
||||
};
|
||||
|
||||
const FETCH_URL = 'https://www.amicale-insat.fr/api/event/list';
|
||||
const AGENDA_MONTH_SPAN = 3;
|
||||
|
||||
/**
|
||||
* Class defining the app's planning screen
|
||||
*/
|
||||
class PlanningScreen extends React.Component<Props, State> {
|
||||
class PlanningScreen extends React.Component<PropsType, StateType> {
|
||||
agendaRef: null | Agenda;
|
||||
|
||||
agendaRef: Object;
|
||||
lastRefresh: Date;
|
||||
|
||||
lastRefresh: Date;
|
||||
minTimeBetweenRefresh = 60;
|
||||
minTimeBetweenRefresh = 60;
|
||||
|
||||
state = {
|
||||
refreshing: false,
|
||||
agendaItems: {},
|
||||
calendarShowing: false,
|
||||
currentDate = getDateOnlyString(getCurrentDateString());
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
if (i18n.currentLocale().startsWith('fr')) {
|
||||
LocaleConfig.defaultLocale = 'fr';
|
||||
}
|
||||
this.state = {
|
||||
refreshing: false,
|
||||
agendaItems: {},
|
||||
calendarShowing: false,
|
||||
};
|
||||
}
|
||||
|
||||
currentDate = getDateOnlyString(getCurrentDateString());
|
||||
/**
|
||||
* Captures focus and blur events to hook on android back button
|
||||
*/
|
||||
componentDidMount() {
|
||||
const {navigation} = this.props;
|
||||
this.onRefresh();
|
||||
navigation.addListener('focus', () => {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
});
|
||||
navigation.addListener('blur', () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
if (i18n.currentLocale().startsWith("fr")) {
|
||||
LocaleConfig.defaultLocale = 'fr';
|
||||
}
|
||||
/**
|
||||
* Overrides default android back button behaviour to close the calendar if it was open.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
const {calendarShowing} = this.state;
|
||||
if (calendarShowing && this.agendaRef != null) {
|
||||
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Captures focus and blur events to hook on android back button
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.onRefresh();
|
||||
this.props.navigation.addListener(
|
||||
'focus',
|
||||
() =>
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
)
|
||||
);
|
||||
this.props.navigation.addListener(
|
||||
'blur',
|
||||
() =>
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
)
|
||||
);
|
||||
/**
|
||||
* Refreshes data and shows an animation while doing it
|
||||
*/
|
||||
onRefresh = () => {
|
||||
let canRefresh;
|
||||
if (this.lastRefresh !== undefined)
|
||||
canRefresh =
|
||||
(new Date().getTime() - this.lastRefresh.getTime()) / 1000 >
|
||||
this.minTimeBetweenRefresh;
|
||||
else canRefresh = true;
|
||||
|
||||
if (canRefresh) {
|
||||
this.setState({refreshing: true});
|
||||
readData(FETCH_URL)
|
||||
.then((fetchedData: Array<PlanningEventType>) => {
|
||||
this.setState({
|
||||
refreshing: false,
|
||||
agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN),
|
||||
});
|
||||
this.lastRefresh = new Date();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
refreshing: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides default android back button behaviour to close the calendar if it was open.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = () => {
|
||||
if (this.state.calendarShowing) {
|
||||
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Callback used when receiving the agenda ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onAgendaRef = (ref: Agenda) => {
|
||||
this.agendaRef = ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when a button is pressed to toggle the calendar
|
||||
*
|
||||
* @param isCalendarOpened True is the calendar is already open, false otherwise
|
||||
*/
|
||||
onCalendarToggled = (isCalendarOpened: boolean) => {
|
||||
this.setState({calendarShowing: isCalendarOpened});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an event render item
|
||||
*
|
||||
* @param item The current event to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = (item: PlanningEventType): React.Node => {
|
||||
const {navigation} = this.props;
|
||||
const onPress = () => {
|
||||
navigation.navigate('planning-information', {
|
||||
data: item,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function used to check if a row has changed
|
||||
*
|
||||
* @param r1
|
||||
* @param r2
|
||||
* @return {boolean}
|
||||
*/
|
||||
rowHasChanged(r1: Object, r2: Object) {
|
||||
return false;
|
||||
// if (r1 !== undefined && r2 !== undefined)
|
||||
// return r1.title !== r2.title;
|
||||
// else return !(r1 === undefined && r2 === undefined);
|
||||
if (item.logo !== null) {
|
||||
return (
|
||||
<View>
|
||||
<Divider />
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={getFormattedEventTime(item.date_begin, item.date_end)}
|
||||
left={(): React.Node => (
|
||||
<Avatar.Image
|
||||
source={{uri: item.logo}}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
)}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<Divider />
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={getFormattedEventTime(item.date_begin, item.date_end)}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes data and shows an animation while doing it
|
||||
*/
|
||||
onRefresh = () => {
|
||||
let canRefresh;
|
||||
if (this.lastRefresh !== undefined)
|
||||
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
|
||||
else
|
||||
canRefresh = true;
|
||||
/**
|
||||
* Gets an empty render item for an empty date
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderEmptyDate = (): React.Node => <Divider />;
|
||||
|
||||
if (canRefresh) {
|
||||
this.setState({refreshing: true});
|
||||
readData(FETCH_URL)
|
||||
.then((fetchedData) => {
|
||||
this.setState({
|
||||
refreshing: false,
|
||||
agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN)
|
||||
});
|
||||
this.lastRefresh = new Date();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
refreshing: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when receiving the agenda ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onAgendaRef = (ref: Object) => {
|
||||
this.agendaRef = ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when a button is pressed to toggle the calendar
|
||||
*
|
||||
* @param isCalendarOpened True is the calendar is already open, false otherwise
|
||||
*/
|
||||
onCalendarToggled = (isCalendarOpened: boolean) => {
|
||||
this.setState({calendarShowing: isCalendarOpened});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an event render item
|
||||
*
|
||||
* @param item The current event to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = (item: eventObject) => {
|
||||
const onPress = this.props.navigation.navigate.bind(this, 'planning-information', {data: item});
|
||||
if (item.logo !== null) {
|
||||
return (
|
||||
<View>
|
||||
<Divider/>
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={getFormattedEventTime(item["date_begin"], item["date_end"])}
|
||||
left={() => <Avatar.Image
|
||||
source={{uri: item.logo}}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View>
|
||||
<Divider/>
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={getFormattedEventTime(item["date_begin"], item["date_end"])}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an empty render item for an empty date
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderEmptyDate = () => <Divider/>;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<CustomAgenda
|
||||
{...this.props}
|
||||
// the list of items that have to be displayed in agenda. If you want to render item as empty date
|
||||
// the value of date key kas to be an empty array []. If there exists no value for date key it is
|
||||
// considered that the date in question is not yet loaded
|
||||
items={this.state.agendaItems}
|
||||
// initially selected day
|
||||
selected={this.currentDate}
|
||||
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
||||
minDate={this.currentDate}
|
||||
// Max amount of months allowed to scroll to the past. Default = 50
|
||||
pastScrollRange={1}
|
||||
// Max amount of months allowed to scroll to the future. Default = 50
|
||||
futureScrollRange={AGENDA_MONTH_SPAN}
|
||||
// If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop correctly.
|
||||
onRefresh={this.onRefresh}
|
||||
// callback that fires when the calendar is opened or closed
|
||||
onCalendarToggled={this.onCalendarToggled}
|
||||
// Set this true while waiting for new data from a refresh
|
||||
refreshing={this.state.refreshing}
|
||||
renderItem={this.getRenderItem}
|
||||
renderEmptyDate={this.getRenderEmptyDate}
|
||||
rowHasChanged={this.rowHasChanged}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
// ref to this agenda in order to handle back button event
|
||||
onRef={this.onAgendaRef}
|
||||
/>
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
|
||||
title={i18n.t("screens.planning.mascotDialog.title")}
|
||||
message={i18n.t("screens.planning.mascotDialog.message")}
|
||||
icon={"party-popper"}
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t("screens.planning.mascotDialog.button"),
|
||||
icon: "check",
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.HAPPY}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {state, props} = this;
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<CustomAgenda
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
// the list of items that have to be displayed in agenda. If you want to render item as empty date
|
||||
// the value of date key kas to be an empty array []. If there exists no value for date key it is
|
||||
// considered that the date in question is not yet loaded
|
||||
items={state.agendaItems}
|
||||
// initially selected day
|
||||
selected={this.currentDate}
|
||||
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
||||
minDate={this.currentDate}
|
||||
// Max amount of months allowed to scroll to the past. Default = 50
|
||||
pastScrollRange={1}
|
||||
// Max amount of months allowed to scroll to the future. Default = 50
|
||||
futureScrollRange={AGENDA_MONTH_SPAN}
|
||||
// If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop correctly.
|
||||
onRefresh={this.onRefresh}
|
||||
// callback that fires when the calendar is opened or closed
|
||||
onCalendarToggled={this.onCalendarToggled}
|
||||
// Set this true while waiting for new data from a refresh
|
||||
refreshing={state.refreshing}
|
||||
renderItem={this.getRenderItem}
|
||||
renderEmptyDate={this.getRenderEmptyDate}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
// ref to this agenda in order to handle back button event
|
||||
onRef={this.onAgendaRef}
|
||||
/>
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
|
||||
title={i18n.t('screens.planning.mascotDialog.title')}
|
||||
message={i18n.t('screens.planning.mascotDialog.message')}
|
||||
icon="party-popper"
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t('screens.planning.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.HAPPY}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlanningScreen;
|
||||
|
|
|
|||
|
|
@ -2,85 +2,117 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Image, View} from 'react-native';
|
||||
import i18n from "i18n-js";
|
||||
import i18n from 'i18n-js';
|
||||
import {Card, List, Paragraph, Text, Title} from 'react-native-paper';
|
||||
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
|
||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proxiwash.png";
|
||||
const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proxiwash.png';
|
||||
|
||||
/**
|
||||
* Class defining the proxiwash about screen.
|
||||
*/
|
||||
export default class ProxiwashAboutScreen extends React.Component<Props> {
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export default class ProxiwashAboutScreen extends React.Component<null> {
|
||||
render(): React.Node {
|
||||
return (
|
||||
<CollapsibleScrollView style={{padding: 5}} hasTab>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 100,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: LOGO}}
|
||||
style={{height: '100%', width: '100%', resizeMode: 'contain'}}
|
||||
/>
|
||||
</View>
|
||||
<Text>{i18n.t('screens.proxiwash.description')}</Text>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.dryer')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="tumble-dryer" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.proxiwash.procedure')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryerProcedure')}</Paragraph>
|
||||
<Title>{i18n.t('screens.proxiwash.tips')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryerTips')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsibleScrollView
|
||||
style={{padding: 5}}
|
||||
hasTab={true}
|
||||
>
|
||||
<View style={{
|
||||
width: '100%',
|
||||
height: 100,
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: LOGO}}
|
||||
style={{height: '100%', width: '100%', resizeMode: "contain"}}/>
|
||||
</View>
|
||||
<Text>{i18n.t('screens.proxiwash.description')}</Text>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.dryer')}
|
||||
left={props => <List.Icon {...props} icon={'tumble-dryer'}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.proxiwash.procedure')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryerProcedure')}</Paragraph>
|
||||
<Title>{i18n.t('screens.proxiwash.tips')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryerTips')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.washer')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="washing-machine" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.proxiwash.procedure')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washerProcedure')}</Paragraph>
|
||||
<Title>{i18n.t('screens.proxiwash.tips')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washerTips')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.washer')}
|
||||
left={props => <List.Icon {...props} icon={'washing-machine'}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Title>{i18n.t('screens.proxiwash.procedure')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washerProcedure')}</Paragraph>
|
||||
<Title>{i18n.t('screens.proxiwash.tips')}</Title>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washerTips')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.tariffs')}
|
||||
left={props => <List.Icon {...props} icon={'circle-multiple'}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washersTariff')}</Paragraph>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryersTariff')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.paymentMethods')}
|
||||
left={props => <List.Icon {...props} icon={'cash'}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.paymentMethodsDescription')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
<Card style={{margin: 5}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.tariffs')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="circle-multiple" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.washersTariff')}</Paragraph>
|
||||
<Paragraph>{i18n.t('screens.proxiwash.dryersTariff')}</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card
|
||||
style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.paymentMethods')}
|
||||
left={({
|
||||
size,
|
||||
color,
|
||||
}: {
|
||||
size: number,
|
||||
color: string,
|
||||
}): React.Node => (
|
||||
<List.Icon size={size} color={color} icon="cash" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('screens.proxiwash.paymentMethodsDescription')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,421 +2,480 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Alert, View} from 'react-native';
|
||||
import i18n from "i18n-js";
|
||||
import WebSectionList from "../../components/Screens/WebSectionList";
|
||||
import * as Notifications from "../../utils/Notifications";
|
||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
||||
import i18n from 'i18n-js';
|
||||
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 type {CustomTheme} from "../../managers/ThemeManager";
|
||||
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";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {Modalize} from 'react-native-modalize';
|
||||
import WebSectionList from '../../components/Screens/WebSectionList';
|
||||
import * as Notifications from '../../utils/Notifications';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
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 type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import {
|
||||
getCleanedMachineWatched,
|
||||
getMachineEndDate,
|
||||
isMachineWatched,
|
||||
} from '../../utils/Proxiwash';
|
||||
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import type {SectionListDataType} from '../../components/Screens/WebSectionList';
|
||||
|
||||
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json";
|
||||
const DATA_URL =
|
||||
'https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json';
|
||||
|
||||
let modalStateStrings = {};
|
||||
const modalStateStrings = {};
|
||||
|
||||
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
||||
export type Machine = {
|
||||
number: string,
|
||||
state: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
donePercent: string,
|
||||
remainingTime: string,
|
||||
program: string,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
type State = {
|
||||
refreshing: boolean,
|
||||
modalCurrentDisplayItem: React.Node,
|
||||
machinesWatched: Array<Machine>,
|
||||
export type ProxiwashMachineType = {
|
||||
number: string,
|
||||
state: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
donePercent: string,
|
||||
remainingTime: string,
|
||||
program: string,
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
modalCurrentDisplayItem: React.Node,
|
||||
machinesWatched: Array<ProxiwashMachineType>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Class defining the app's proxiwash screen. This screen shows information about washing machines and
|
||||
* dryers, taken from a scrapper reading proxiwash website
|
||||
*/
|
||||
class ProxiwashScreen extends React.Component<Props, State> {
|
||||
class ProxiwashScreen extends React.Component<PropsType, StateType> {
|
||||
/**
|
||||
* Shows a warning telling the user notifications are disabled for the app
|
||||
*/
|
||||
static showNotificationsDisabledWarning() {
|
||||
Alert.alert(
|
||||
i18n.t('screens.proxiwash.modal.notificationErrorTitle'),
|
||||
i18n.t('screens.proxiwash.modal.notificationErrorDescription'),
|
||||
);
|
||||
}
|
||||
|
||||
modalRef: null | Modalize;
|
||||
modalRef: null | Modalize;
|
||||
|
||||
fetchedData: {
|
||||
dryers: Array<Machine>,
|
||||
washers: Array<Machine>,
|
||||
fetchedData: {
|
||||
dryers: Array<ProxiwashMachineType>,
|
||||
washers: Array<ProxiwashMachineType>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates machine state parameters using current theme and translations
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
modalCurrentDisplayItem: null,
|
||||
machinesWatched: AsyncStorageManager.getObject(
|
||||
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
|
||||
),
|
||||
};
|
||||
modalStateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
|
||||
'screens.proxiwash.modal.ready',
|
||||
);
|
||||
modalStateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
|
||||
'screens.proxiwash.modal.running',
|
||||
);
|
||||
modalStateStrings[
|
||||
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
|
||||
] = i18n.t('screens.proxiwash.modal.runningNotStarted');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
|
||||
'screens.proxiwash.modal.finished',
|
||||
);
|
||||
modalStateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
|
||||
'screens.proxiwash.modal.broken',
|
||||
);
|
||||
modalStateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
|
||||
'screens.proxiwash.modal.error',
|
||||
);
|
||||
modalStateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
|
||||
'screens.proxiwash.modal.unknown',
|
||||
);
|
||||
}
|
||||
|
||||
state = {
|
||||
refreshing: false,
|
||||
modalCurrentDisplayItem: null,
|
||||
machinesWatched: AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key),
|
||||
/**
|
||||
* Setup notification channel for android and add listeners to detect notifications fired
|
||||
*/
|
||||
componentDidMount() {
|
||||
const {navigation} = this.props;
|
||||
navigation.setOptions({
|
||||
headerRight: (): React.Node => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="information"
|
||||
iconName="information"
|
||||
onPress={this.onAboutPress}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when pressing the about button.
|
||||
* This will open the ProxiwashAboutScreen.
|
||||
*/
|
||||
onAboutPress = () => {
|
||||
const {navigation} = this.props;
|
||||
navigation.navigate('proxiwash-about');
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when the user clicks on enable notifications for a machine
|
||||
*
|
||||
* @param machine The machine to set notifications for
|
||||
*/
|
||||
onSetupNotificationsPress(machine: ProxiwashMachineType) {
|
||||
if (this.modalRef) {
|
||||
this.modalRef.close();
|
||||
}
|
||||
this.setupNotifications(machine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when receiving modal ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onModalRef = (ref: Modalize) => {
|
||||
this.modalRef = ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the modal content.
|
||||
* This shows information for the given machine.
|
||||
*
|
||||
* @param title The title to use
|
||||
* @param item The item to display information for in the modal
|
||||
* @param isDryer True if the given item is a dryer
|
||||
* @return {*}
|
||||
*/
|
||||
getModalContent(
|
||||
title: string,
|
||||
item: ProxiwashMachineType,
|
||||
isDryer: boolean,
|
||||
): React.Node {
|
||||
const {props, state} = this;
|
||||
let button = {
|
||||
text: i18n.t('screens.proxiwash.modal.ok'),
|
||||
icon: '',
|
||||
onPress: undefined,
|
||||
};
|
||||
let message = modalStateStrings[item.state];
|
||||
const onPress = this.onSetupNotificationsPress.bind(this, item);
|
||||
if (item.state === ProxiwashConstants.machineStates.RUNNING) {
|
||||
let remainingTime = parseInt(item.remainingTime, 10);
|
||||
if (remainingTime < 0) remainingTime = 0;
|
||||
|
||||
/**
|
||||
* Creates machine state parameters using current theme and translations
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
modalStateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.modal.ready');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.modal.running');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.modal.runningNotStarted');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.modal.finished');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.modal.broken');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.modal.error');
|
||||
modalStateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.modal.unknown');
|
||||
button = {
|
||||
text: isMachineWatched(item, state.machinesWatched)
|
||||
? i18n.t('screens.proxiwash.modal.disableNotifications')
|
||||
: i18n.t('screens.proxiwash.modal.enableNotifications'),
|
||||
icon: '',
|
||||
onPress,
|
||||
};
|
||||
message = i18n.t('screens.proxiwash.modal.running', {
|
||||
start: item.startTime,
|
||||
end: item.endTime,
|
||||
remaining: remainingTime,
|
||||
program: item.program,
|
||||
});
|
||||
} else if (item.state === ProxiwashConstants.machineStates.AVAILABLE) {
|
||||
if (isDryer) message += `\n${i18n.t('screens.proxiwash.dryersTariff')}`;
|
||||
else message += `\n${i18n.t('screens.proxiwash.washersTariff')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup notification channel for android and add listeners to detect notifications fired
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerRight: () =>
|
||||
<MaterialHeaderButtons>
|
||||
<Item title="information" iconName="information" onPress={this.onAboutPress}/>
|
||||
</MaterialHeaderButtons>,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when pressing the about button.
|
||||
* This will open the ProxiwashAboutScreen.
|
||||
*/
|
||||
onAboutPress = () => this.props.navigation.navigate('proxiwash-about');
|
||||
|
||||
/**
|
||||
* Extracts the key for the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {*} The extracted key
|
||||
*/
|
||||
getKeyExtractor = (item: Machine) => item.number;
|
||||
|
||||
/**
|
||||
* Setups notifications for the machine with the given ID.
|
||||
* One notification will be sent at the end of the program.
|
||||
* Another will be send a few minutes before the end, based on the value of reminderNotifTime
|
||||
*
|
||||
* @param machine The machine to watch
|
||||
*/
|
||||
setupNotifications(machine: Machine) {
|
||||
if (!isMachineWatched(machine, this.state.machinesWatched)) {
|
||||
Notifications.setupMachineNotification(machine.number, true, getMachineEndDate(machine))
|
||||
.then(() => {
|
||||
this.saveNotificationToState(machine);
|
||||
})
|
||||
.catch(() => {
|
||||
this.showNotificationsDisabledWarning();
|
||||
});
|
||||
} else {
|
||||
Notifications.setupMachineNotification(machine.number, false, null)
|
||||
.then(() => {
|
||||
this.removeNotificationFromState(machine);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a warning telling the user notifications are disabled for the app
|
||||
*/
|
||||
showNotificationsDisabledWarning() {
|
||||
Alert.alert(
|
||||
i18n.t("screens.proxiwash.modal.notificationErrorTitle"),
|
||||
i18n.t("screens.proxiwash.modal.notificationErrorDescription"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
|
||||
*
|
||||
* @param machine
|
||||
*/
|
||||
saveNotificationToState(machine: Machine) {
|
||||
let data = this.state.machinesWatched;
|
||||
data.push(machine);
|
||||
this.saveNewWatchedList(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given index from the watchlist array and saves it to preferences
|
||||
*
|
||||
* @param machine
|
||||
*/
|
||||
removeNotificationFromState(machine: Machine) {
|
||||
let data = this.state.machinesWatched;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].number === machine.number && data[i].endTime === machine.endTime) {
|
||||
data.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.saveNewWatchedList(data);
|
||||
}
|
||||
|
||||
saveNewWatchedList(list: Array<Machine>) {
|
||||
this.setState({machinesWatched: list});
|
||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dataset to be used by the flatlist
|
||||
*
|
||||
* @param fetchedData
|
||||
* @return {*}
|
||||
*/
|
||||
createDataset = (fetchedData: Object) => {
|
||||
let data = fetchedData;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
|
||||
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
|
||||
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
|
||||
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
|
||||
}
|
||||
this.fetchedData = data;
|
||||
this.state.machinesWatched =
|
||||
getCleanedMachineWatched(this.state.machinesWatched, [...data.dryers, ...data.washers]);
|
||||
return [
|
||||
{
|
||||
title: i18n.t('screens.proxiwash.dryers'),
|
||||
icon: 'tumble-dryer',
|
||||
data: data.dryers === undefined ? [] : data.dryers,
|
||||
keyExtractor: this.getKeyExtractor
|
||||
},
|
||||
{
|
||||
title: i18n.t('screens.proxiwash.washers'),
|
||||
icon: 'washing-machine',
|
||||
data: data.washers === undefined ? [] : data.washers,
|
||||
keyExtractor: this.getKeyExtractor
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a modal for the given item
|
||||
*
|
||||
* @param title The title to use
|
||||
* @param item The item to display information for in the modal
|
||||
* @param isDryer True if the given item is a dryer
|
||||
*/
|
||||
showModal = (title: string, item: Object, isDryer: boolean) => {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: this.getModalContent(title, item, isDryer)
|
||||
});
|
||||
if (this.modalRef) {
|
||||
this.modalRef.open();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when the user clicks on enable notifications for a machine
|
||||
*
|
||||
* @param machine The machine to set notifications for
|
||||
*/
|
||||
onSetupNotificationsPress(machine: Machine) {
|
||||
if (this.modalRef) {
|
||||
this.modalRef.close();
|
||||
}
|
||||
this.setupNotifications(machine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the modal content.
|
||||
* This shows information for the given machine.
|
||||
*
|
||||
* @param title The title to use
|
||||
* @param item The item to display information for in the modal
|
||||
* @param isDryer True if the given item is a dryer
|
||||
* @return {*}
|
||||
*/
|
||||
getModalContent(title: string, item: Machine, isDryer: boolean) {
|
||||
let button = {
|
||||
text: i18n.t("screens.proxiwash.modal.ok"),
|
||||
icon: '',
|
||||
onPress: undefined
|
||||
};
|
||||
let message = modalStateStrings[item.state];
|
||||
const onPress = this.onSetupNotificationsPress.bind(this, item);
|
||||
if (item.state === ProxiwashConstants.machineStates.RUNNING) {
|
||||
let remainingTime = parseInt(item.remainingTime)
|
||||
if (remainingTime < 0)
|
||||
remainingTime = 0;
|
||||
|
||||
button =
|
||||
{
|
||||
text: isMachineWatched(item, this.state.machinesWatched) ?
|
||||
i18n.t("screens.proxiwash.modal.disableNotifications") :
|
||||
i18n.t("screens.proxiwash.modal.enableNotifications"),
|
||||
icon: '',
|
||||
onPress: onPress
|
||||
}
|
||||
;
|
||||
message = i18n.t('screens.proxiwash.modal.running',
|
||||
{
|
||||
start: item.startTime,
|
||||
end: item.endTime,
|
||||
remaining: remainingTime,
|
||||
program: item.program
|
||||
});
|
||||
} else if (item.state === ProxiwashConstants.machineStates.AVAILABLE) {
|
||||
if (isDryer)
|
||||
message += '\n' + i18n.t('screens.proxiwash.dryersTariff');
|
||||
else
|
||||
message += '\n' + i18n.t('screens.proxiwash.washersTariff');
|
||||
}
|
||||
return (
|
||||
<View style={{
|
||||
flex: 1,
|
||||
padding: 20
|
||||
}}>
|
||||
<Card.Title
|
||||
title={title}
|
||||
left={() => <Avatar.Icon
|
||||
icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
color={this.props.theme.colors.text}
|
||||
style={{backgroundColor: 'transparent'}}/>}
|
||||
|
||||
/>
|
||||
<Card.Content>
|
||||
<Text>{message}</Text>
|
||||
</Card.Content>
|
||||
|
||||
{button.onPress !== undefined ?
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon={button.icon}
|
||||
mode="contained"
|
||||
onPress={button.onPress}
|
||||
style={{marginLeft: 'auto', marginRight: 'auto'}}
|
||||
>
|
||||
{button.text}
|
||||
</Button>
|
||||
</Card.Actions> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when receiving modal ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onModalRef = (ref: Object) => {
|
||||
this.modalRef = ref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of machines available
|
||||
*
|
||||
* @param isDryer True if we are only checking for dryer, false for washers
|
||||
* @return {number} The number of machines available
|
||||
*/
|
||||
getMachineAvailableNumber(isDryer: boolean) {
|
||||
let data;
|
||||
if (isDryer)
|
||||
data = this.fetchedData.dryers;
|
||||
else
|
||||
data = this.fetchedData.washers;
|
||||
let count = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].state === ProxiwashConstants.machineStates.AVAILABLE)
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the section render item
|
||||
*
|
||||
* @param section The section to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({section}: Object) => {
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
const nbAvailable = this.getMachineAvailableNumber(isDryer);
|
||||
return (
|
||||
<ProxiwashSectionHeader
|
||||
title={section.title}
|
||||
nbAvailable={nbAvailable}
|
||||
isDryer={isDryer}/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list item to be rendered
|
||||
*
|
||||
* @param item The object containing the item's FetchedData
|
||||
* @param section The object describing the current SectionList section
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getRenderItem = ({item, section}: Object) => {
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
return (
|
||||
<ProxiwashListItem
|
||||
item={item}
|
||||
onPress={this.showModal}
|
||||
isWatched={isMachineWatched(item, this.state.machinesWatched)}
|
||||
isDryer={isDryer}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={title}
|
||||
left={(): React.Node => (
|
||||
<Avatar.Icon
|
||||
icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||
color={props.theme.colors.text}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Text>{message}</Text>
|
||||
</Card.Content>
|
||||
|
||||
render() {
|
||||
const nav = this.props.navigation;
|
||||
return (
|
||||
<View
|
||||
style={{flex: 1}}
|
||||
>
|
||||
<View style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}>
|
||||
<WebSectionList
|
||||
createDataset={this.createDataset}
|
||||
navigation={nav}
|
||||
fetchUrl={DATA_URL}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus={true}
|
||||
updateData={this.state.machinesWatched.length}/>
|
||||
</View>
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key}
|
||||
title={i18n.t("screens.proxiwash.mascotDialog.title")}
|
||||
message={i18n.t("screens.proxiwash.mascotDialog.message")}
|
||||
icon={"information"}
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t("screens.proxiwash.mascotDialog.ok"),
|
||||
icon: "check",
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<CustomModal onRef={this.onModalRef}>
|
||||
{this.state.modalCurrentDisplayItem}
|
||||
</CustomModal>
|
||||
</View>
|
||||
);
|
||||
{button.onPress !== undefined ? (
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon={button.icon}
|
||||
mode="contained"
|
||||
onPress={button.onPress}
|
||||
style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
{button.text}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the section render item
|
||||
*
|
||||
* @param section The section to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({
|
||||
section,
|
||||
}: {
|
||||
section: {title: string},
|
||||
}): React.Node => {
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
const nbAvailable = this.getMachineAvailableNumber(isDryer);
|
||||
return (
|
||||
<ProxiwashSectionHeader
|
||||
title={section.title}
|
||||
nbAvailable={nbAvailable}
|
||||
isDryer={isDryer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list item to be rendered
|
||||
*
|
||||
* @param item The object containing the item's FetchedData
|
||||
* @param section The object describing the current SectionList section
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getRenderItem = ({
|
||||
item,
|
||||
section,
|
||||
}: {
|
||||
item: ProxiwashMachineType,
|
||||
section: {title: string},
|
||||
}): React.Node => {
|
||||
const {machinesWatched} = this.state;
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
return (
|
||||
<ProxiwashListItem
|
||||
item={item}
|
||||
onPress={this.showModal}
|
||||
isWatched={isMachineWatched(item, machinesWatched)}
|
||||
isDryer={isDryer}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the key for the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {*} The extracted key
|
||||
*/
|
||||
getKeyExtractor = (item: ProxiwashMachineType): string => item.number;
|
||||
|
||||
/**
|
||||
* Setups notifications for the machine with the given ID.
|
||||
* One notification will be sent at the end of the program.
|
||||
* Another will be send a few minutes before the end, based on the value of reminderNotifTime
|
||||
*
|
||||
* @param machine The machine to watch
|
||||
*/
|
||||
setupNotifications(machine: ProxiwashMachineType) {
|
||||
const {machinesWatched} = this.state;
|
||||
if (!isMachineWatched(machine, machinesWatched)) {
|
||||
Notifications.setupMachineNotification(
|
||||
machine.number,
|
||||
true,
|
||||
getMachineEndDate(machine),
|
||||
)
|
||||
.then(() => {
|
||||
this.saveNotificationToState(machine);
|
||||
})
|
||||
.catch(() => {
|
||||
ProxiwashScreen.showNotificationsDisabledWarning();
|
||||
});
|
||||
} else {
|
||||
Notifications.setupMachineNotification(machine.number, false, null).then(
|
||||
() => {
|
||||
this.removeNotificationFromState(machine);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of machines available
|
||||
*
|
||||
* @param isDryer True if we are only checking for dryer, false for washers
|
||||
* @return {number} The number of machines available
|
||||
*/
|
||||
getMachineAvailableNumber(isDryer: boolean): number {
|
||||
let data;
|
||||
if (isDryer) data = this.fetchedData.dryers;
|
||||
else data = this.fetchedData.washers;
|
||||
let count = 0;
|
||||
data.forEach((machine: ProxiwashMachineType) => {
|
||||
if (machine.state === ProxiwashConstants.machineStates.AVAILABLE)
|
||||
count += 1;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dataset to be used by the FlatList
|
||||
*
|
||||
* @param fetchedData
|
||||
* @return {*}
|
||||
*/
|
||||
createDataset = (fetchedData: {
|
||||
dryers: Array<ProxiwashMachineType>,
|
||||
washers: Array<ProxiwashMachineType>,
|
||||
}): SectionListDataType<ProxiwashMachineType> => {
|
||||
const {state} = this;
|
||||
let data = fetchedData;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
|
||||
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
|
||||
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
|
||||
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
|
||||
}
|
||||
this.fetchedData = data;
|
||||
this.state.machinesWatched = getCleanedMachineWatched(
|
||||
state.machinesWatched,
|
||||
[...data.dryers, ...data.washers],
|
||||
);
|
||||
return [
|
||||
{
|
||||
title: i18n.t('screens.proxiwash.dryers'),
|
||||
icon: 'tumble-dryer',
|
||||
data: data.dryers === undefined ? [] : data.dryers,
|
||||
keyExtractor: this.getKeyExtractor,
|
||||
},
|
||||
{
|
||||
title: i18n.t('screens.proxiwash.washers'),
|
||||
icon: 'washing-machine',
|
||||
data: data.washers === undefined ? [] : data.washers,
|
||||
keyExtractor: this.getKeyExtractor,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a modal for the given item
|
||||
*
|
||||
* @param title The title to use
|
||||
* @param item The item to display information for in the modal
|
||||
* @param isDryer True if the given item is a dryer
|
||||
*/
|
||||
showModal = (title: string, item: ProxiwashMachineType, isDryer: boolean) => {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: this.getModalContent(title, item, isDryer),
|
||||
});
|
||||
if (this.modalRef) {
|
||||
this.modalRef.open();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
|
||||
*
|
||||
* @param machine
|
||||
*/
|
||||
saveNotificationToState(machine: ProxiwashMachineType) {
|
||||
const {machinesWatched} = this.state;
|
||||
const data = machinesWatched;
|
||||
data.push(machine);
|
||||
this.saveNewWatchedList(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given index from the watchlist array and saves it to preferences
|
||||
*
|
||||
* @param selectedMachine
|
||||
*/
|
||||
removeNotificationFromState(selectedMachine: ProxiwashMachineType) {
|
||||
const {machinesWatched} = this.state;
|
||||
const newList = [...machinesWatched];
|
||||
machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => {
|
||||
if (
|
||||
machine.number === selectedMachine.number &&
|
||||
machine.endTime === selectedMachine.endTime
|
||||
)
|
||||
newList.splice(index, 1);
|
||||
});
|
||||
this.saveNewWatchedList(newList);
|
||||
}
|
||||
|
||||
saveNewWatchedList(list: Array<ProxiwashMachineType>) {
|
||||
this.setState({machinesWatched: list});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key,
|
||||
list,
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {state} = this;
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<WebSectionList
|
||||
createDataset={this.createDataset}
|
||||
navigation={navigation}
|
||||
fetchUrl={DATA_URL}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus
|
||||
updateData={state.machinesWatched.length}
|
||||
/>
|
||||
</View>
|
||||
<MascotPopup
|
||||
prefKey={AsyncStorageManager.PREFERENCES.proxiwashShowBanner.key}
|
||||
title={i18n.t('screens.proxiwash.mascotDialog.title')}
|
||||
message={i18n.t('screens.proxiwash.mascotDialog.message')}
|
||||
icon="information"
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t('screens.proxiwash.mascotDialog.ok'),
|
||||
icon: 'check',
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<CustomModal onRef={this.onModalRef}>
|
||||
{state.modalCurrentDisplayItem}
|
||||
</CustomModal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ProxiwashScreen);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
|
||||
import type {ProximoArticleType} from './ProximoMainScreen';
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ type PropsType = {
|
|||
shouldFocusSearchBar: boolean,
|
||||
},
|
||||
},
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import WebSectionList from '../../../components/Screens/WebSectionList';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {SectionListDataType} from '../../../components/Screens/WebSectionList';
|
||||
|
||||
const DATA_URL = 'https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json';
|
||||
|
|
@ -43,7 +43,7 @@ export type ProximoDataType = {
|
|||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,167 +2,188 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import DateManager from "../../managers/DateManager";
|
||||
import WebSectionList from "../../components/Screens/WebSectionList";
|
||||
import {Card, Text, withTheme} from 'react-native-paper';
|
||||
import AprilFoolsManager from "../../managers/AprilFoolsManager";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
import DateManager from '../../managers/DateManager';
|
||||
import WebSectionList from '../../components/Screens/WebSectionList';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {SectionListDataType} from '../../components/Screens/WebSectionList';
|
||||
|
||||
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json";
|
||||
const DATA_URL =
|
||||
'https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
export type RuFoodCategoryType = {
|
||||
name: string,
|
||||
dishes: Array<{name: string}>,
|
||||
};
|
||||
|
||||
type RuMealType = {
|
||||
name: string,
|
||||
foodcategory: Array<RuFoodCategoryType>,
|
||||
};
|
||||
|
||||
type RawRuMenuType = {
|
||||
restaurant_id: number,
|
||||
id: number,
|
||||
date: string,
|
||||
meal: Array<RuMealType>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Class defining the app's menu screen.
|
||||
*/
|
||||
class SelfMenuScreen extends React.Component<Props> {
|
||||
class SelfMenuScreen extends React.Component<PropsType> {
|
||||
/**
|
||||
* Formats the given string to make sure it starts with a capital letter
|
||||
*
|
||||
* @param name The string to format
|
||||
* @return {string} The formatted string
|
||||
*/
|
||||
static formatName(name: string): string {
|
||||
return name.charAt(0) + name.substr(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a key for the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {*} The extracted key
|
||||
*/
|
||||
getKeyExtractor(item: Object) {
|
||||
return item !== undefined ? item['name'] : undefined;
|
||||
/**
|
||||
* Creates the dataset to be used in the FlatList
|
||||
*
|
||||
* @param fetchedData
|
||||
* @return {[]}
|
||||
*/
|
||||
createDataset = (
|
||||
fetchedData: Array<RawRuMenuType>,
|
||||
): SectionListDataType<RuFoodCategoryType> => {
|
||||
let result = [];
|
||||
if (fetchedData == null || fetchedData.length === 0) {
|
||||
result = [
|
||||
{
|
||||
title: i18n.t('general.notAvailable'),
|
||||
data: [],
|
||||
keyExtractor: this.getKeyExtractor,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
fetchedData.forEach((item: RawRuMenuType) => {
|
||||
result.push({
|
||||
title: DateManager.getInstance().getTranslatedDate(item.date),
|
||||
data: item.meal[0].foodcategory,
|
||||
keyExtractor: this.getKeyExtractor,
|
||||
});
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the dataset to be used in the FlatList
|
||||
*
|
||||
* @param fetchedData
|
||||
* @return {[]}
|
||||
*/
|
||||
createDataset = (fetchedData: Object) => {
|
||||
let result = [];
|
||||
if (fetchedData == null || Object.keys(fetchedData).length === 0) {
|
||||
result = [
|
||||
{
|
||||
title: i18n.t("general.notAvailable"),
|
||||
data: [],
|
||||
keyExtractor: this.getKeyExtractor
|
||||
}
|
||||
];
|
||||
} else {
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled() && fetchedData.length > 0)
|
||||
fetchedData[0].meal[0].foodcategory = AprilFoolsManager.getFakeMenuItem(fetchedData[0].meal[0].foodcategory);
|
||||
// fetched data is an array here
|
||||
for (let i = 0; i < fetchedData.length; i++) {
|
||||
result.push(
|
||||
{
|
||||
title: DateManager.getInstance().getTranslatedDate(fetchedData[i].date),
|
||||
data: fetchedData[i].meal[0].foodcategory,
|
||||
keyExtractor: this.getKeyExtractor,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return result
|
||||
};
|
||||
/**
|
||||
* Gets the render section header
|
||||
*
|
||||
* @param section The section to render the header from
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({
|
||||
section,
|
||||
}: {
|
||||
section: {title: string},
|
||||
}): React.Node => {
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
width: '95%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
elevation: 4,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={section.title}
|
||||
titleStyle={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
subtitleStyle={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the render section header
|
||||
*
|
||||
* @param section The section to render the header from
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({section}: Object) => {
|
||||
return (
|
||||
<Card style={{
|
||||
width: '95%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
elevation: 4,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={section.title}
|
||||
titleStyle={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
subtitleStyle={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Gets a FlatList render item
|
||||
*
|
||||
* @param item The item to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = ({item}: {item: RuFoodCategoryType}): React.Node => {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
flex: 0,
|
||||
marginHorizontal: 10,
|
||||
marginVertical: 5,
|
||||
}}>
|
||||
<Card.Title style={{marginTop: 5}} title={item.name} />
|
||||
<View
|
||||
style={{
|
||||
width: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.primary,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
/>
|
||||
<Card.Content>
|
||||
{item.dishes.map((object: {name: string}): React.Node =>
|
||||
object.name !== '' ? (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{SelfMenuScreen.formatName(object.name)}
|
||||
</Text>
|
||||
) : null,
|
||||
)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a FlatList render item
|
||||
*
|
||||
* @param item The item to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = ({item}: Object) => {
|
||||
return (
|
||||
<Card style={{
|
||||
flex: 0,
|
||||
marginHorizontal: 10,
|
||||
marginVertical: 5,
|
||||
}}>
|
||||
<Card.Title
|
||||
style={{marginTop: 5}}
|
||||
title={item.name}
|
||||
/>
|
||||
<View style={{
|
||||
width: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: this.props.theme.colors.primary,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
}}/>
|
||||
<Card.Content>
|
||||
{item.dishes.map((object) =>
|
||||
<View>
|
||||
{object.name !== "" ?
|
||||
<Text style={{
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
textAlign: 'center'
|
||||
}}>{this.formatName(object.name)}</Text>
|
||||
: <View/>}
|
||||
</View>)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Extract a key for the given item
|
||||
*
|
||||
* @param item The item to extract the key from
|
||||
* @return {*} The extracted key
|
||||
*/
|
||||
getKeyExtractor = (item: RuFoodCategoryType): string => item.name;
|
||||
|
||||
/**
|
||||
* Formats the given string to make sure it starts with a capital letter
|
||||
*
|
||||
* @param name The string to format
|
||||
* @return {string} The formatted string
|
||||
*/
|
||||
formatName(name: String) {
|
||||
return name.charAt(0) + name.substr(1).toLowerCase();
|
||||
}
|
||||
|
||||
render() {
|
||||
const nav = this.props.navigation;
|
||||
return (
|
||||
<WebSectionList
|
||||
createDataset={this.createDataset}
|
||||
navigation={nav}
|
||||
autoRefreshTime={0}
|
||||
refreshOnFocus={false}
|
||||
fetchUrl={DATA_URL}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
stickyHeader={true}/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<WebSectionList
|
||||
createDataset={this.createDataset}
|
||||
navigation={navigation}
|
||||
autoRefreshTime={0}
|
||||
refreshOnFocus={false}
|
||||
fetchUrl={DATA_URL}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
stickyHeader
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SelfMenuScreen);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import CardList from '../../components/Lists/CardList/CardList';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
|
|
@ -28,7 +28,7 @@ import type {ServiceCategoryType} from '../../managers/ServicesManager';
|
|||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class ServicesScreen extends React.Component<PropsType> {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {CommonActions} from '@react-navigation/native';
|
|||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import CardList from '../../components/Lists/CardList/CardList';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import {withCollapsible} from '../../utils/withCollapsible';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import type {ServiceCategoryType} from '../../managers/ServicesManager';
|
||||
|
||||
type PropsType = {
|
||||
|
|
|
|||
|
|
@ -1,109 +1,116 @@
|
|||
// @flow
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import WebViewScreen from "../../components/Screens/WebViewScreen";
|
||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
||||
import BasicLoadingScreen from "../../components/Screens/BasicLoadingScreen";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import WebViewScreen from '../../components/Screens/WebViewScreen';
|
||||
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: { params: { host: string, path: string | null, title: string } },
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {host: string, path: string | null, title: string}},
|
||||
};
|
||||
|
||||
class WebsiteScreen extends React.Component<Props> {
|
||||
const ENABLE_MOBILE_STRING = `<meta name="viewport" content="width=device-width, initial-scale=1.0">`;
|
||||
|
||||
fullUrl: string;
|
||||
injectedJS: { [key: string]: string };
|
||||
customPaddingFunctions: {[key: string]: (padding: string) => string}
|
||||
const AVAILABLE_ROOMS_STYLE = `<style>body,body>.container2{padding-top:0;width:100%}b,body>.container2>h1,body>.container2>h3,br,header{display:none}.table-bordered td,.table-bordered th{border:none;border-right:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.table{padding:0;margin:0;width:200%;max-width:200%;display:block}tbody{display:block;width:100%}thead{display:block;width:100%}.table tbody tr,tbody tr[bgcolor],thead tr{width:100%;display:inline-flex}.table tbody td,.table thead td[colspan]{padding:0;flex:1;height:50px;margin:0}.table tbody td[bgcolor=white],.table thead td,.table>tbody>tr>td:nth-child(1){flex:0 0 150px;height:50px}</style>`;
|
||||
const BIB_STYLE = `<style>.hero-unit,.navbar,footer{display:none}.hero-unit-form,.hero-unit2,.hero-unit3{background-color:#fff;box-shadow:none;padding:0;margin:0}.hero-unit-form h4{font-size:2rem;line-height:2rem}.btn{font-size:1.5rem;line-height:1.5rem;padding:20px}.btn-danger{background-image:none;background-color:#be1522}.table{font-size:.8rem}.table td{padding:0;height:18.2333px;border:none;border-bottom:1px solid #c1c1c1}.table td[style="max-width:55px;"]{max-width:110px!important}.table-bordered{min-width:50px}th{height:50px}.table-bordered{border-collapse:collapse}</style>`;
|
||||
|
||||
host: string;
|
||||
const BIB_BACK_BUTTON =
|
||||
`<div style='width: 100%; display: flex'>` +
|
||||
`<a style='margin: auto' href='${AvailableWebsites.websites.BIB}'>` +
|
||||
`<button id='customBackButton' class='btn btn-primary'>Retour</button>` +
|
||||
`</a>` +
|
||||
`</div>`;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.injectedJS = {};
|
||||
this.customPaddingFunctions = {};
|
||||
this.injectedJS[AvailableWebsites.websites.AVAILABLE_ROOMS] =
|
||||
'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' +
|
||||
'document.querySelector(\'head\').innerHTML += \'<style>body,body>.container2{padding-top:0;width:100%}b,body>.container2>h1,body>.container2>h3,br,header{display:none}.table-bordered td,.table-bordered th{border:none;border-right:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.table{padding:0;margin:0;width:200%;max-width:200%;display:block}tbody{display:block;width:100%}thead{display:block;width:100%}.table tbody tr,tbody tr[bgcolor],thead tr{width:100%;display:inline-flex}.table tbody td,.table thead td[colspan]{padding:0;flex:1;height:50px;margin:0}.table tbody td[bgcolor=white],.table thead td,.table>tbody>tr>td:nth-child(1){flex:0 0 150px;height:50px}</style>\'; true;';
|
||||
class WebsiteScreen extends React.Component<PropsType> {
|
||||
fullUrl: string;
|
||||
|
||||
this.injectedJS[AvailableWebsites.websites.BIB] =
|
||||
'document.querySelector(\'head\').innerHTML += \'<meta name="viewport" content="width=device-width, initial-scale=1.0">\';' +
|
||||
'document.querySelector(\'head\').innerHTML += \'<style>.hero-unit,.navbar,footer{display:none}.hero-unit-form,.hero-unit2,.hero-unit3{background-color:#fff;box-shadow:none;padding:0;margin:0}.hero-unit-form h4{font-size:2rem;line-height:2rem}.btn{font-size:1.5rem;line-height:1.5rem;padding:20px}.btn-danger{background-image:none;background-color:#be1522}.table{font-size:.8rem}.table td{padding:0;height:18.2333px;border:none;border-bottom:1px solid #c1c1c1}.table td[style="max-width:55px;"]{max-width:110px!important}.table-bordered{min-width:50px}th{height:50px}.table-bordered{border-collapse:collapse}</style>\';' +
|
||||
'if ($(".hero-unit-form").length > 0 && $("#customBackButton").length === 0)' +
|
||||
'$(".hero-unit-form").append("' +
|
||||
'<div style=\'width: 100%; display: flex\'>' +
|
||||
'<a style=\'margin: auto\' href=\'' + AvailableWebsites.websites.BIB + '\'>' +
|
||||
'<button id=\'customBackButton\' class=\'btn btn-primary\'>Retour</button>' +
|
||||
'</a>' +
|
||||
'</div>");true;';
|
||||
injectedJS: {[key: string]: string};
|
||||
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = (padding: string) => {
|
||||
return (
|
||||
"$('head').append('<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">');" +
|
||||
"$('.minwidth').css('top', " + padding + ");" +
|
||||
"$('#mailview-bottom').css('min-height', 500);"
|
||||
);
|
||||
};
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = (padding: string) => {
|
||||
return (
|
||||
"$('#p-logo-text').css('top', 10 + " + padding + ");" +
|
||||
"$('#site-navigation h2').css('top', 10 + " + padding + ");" +
|
||||
"$('#site-tools h2').css('top', 10 + " + padding + ");" +
|
||||
"$('#user-tools h2').css('top', 10 + " + padding + ");"
|
||||
);
|
||||
}
|
||||
}
|
||||
customPaddingFunctions: {[key: string]: (padding: string) => string};
|
||||
|
||||
onScreenFocus = () => {
|
||||
this.handleNavigationParams();
|
||||
host: string;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.injectedJS = {};
|
||||
this.customPaddingFunctions = {};
|
||||
this.injectedJS[AvailableWebsites.websites.AVAILABLE_ROOMS] =
|
||||
`document.querySelector('head').innerHTML += '${ENABLE_MOBILE_STRING}';` +
|
||||
`document.querySelector('head').innerHTML += '${AVAILABLE_ROOMS_STYLE}'; true;`;
|
||||
|
||||
this.injectedJS[AvailableWebsites.websites.BIB] =
|
||||
`document.querySelector('head').innerHTML += '${ENABLE_MOBILE_STRING}';` +
|
||||
`document.querySelector('head').innerHTML += '${BIB_STYLE}';` +
|
||||
`if ($(".hero-unit-form").length > 0 && $("#customBackButton").length === 0)` +
|
||||
`$(".hero-unit-form").append("${BIB_BACK_BUTTON}");true;`;
|
||||
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = (
|
||||
padding: string,
|
||||
): string => {
|
||||
return (
|
||||
`$('head').append('${ENABLE_MOBILE_STRING}');` +
|
||||
`$('.minwidth').css('top', ${padding}` +
|
||||
`$('#mailview-bottom').css('min-height', 500);`
|
||||
);
|
||||
};
|
||||
this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = (
|
||||
padding: string,
|
||||
): string => {
|
||||
return (
|
||||
`$('#p-logo-text').css('top', 10 + ${padding});` +
|
||||
`$('#site-navigation h2').css('top', 10 + ${padding});` +
|
||||
`$('#site-tools h2').css('top', 10 + ${padding});` +
|
||||
`$('#user-tools h2').css('top', 10 + ${padding});`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
handleNavigationParams() {
|
||||
if (this.props.route.params != null) {
|
||||
this.host = this.props.route.params.host;
|
||||
let path = this.props.route.params.path;
|
||||
const title = this.props.route.params.title;
|
||||
if (this.host != null && path != null) {
|
||||
path = path.replace(this.host, "");
|
||||
this.fullUrl = this.host + path;
|
||||
}else
|
||||
this.fullUrl = this.host;
|
||||
onScreenFocus = () => {
|
||||
this.handleNavigationParams();
|
||||
};
|
||||
|
||||
if (title != null)
|
||||
this.props.navigation.setOptions({title: title});
|
||||
}
|
||||
/**
|
||||
*
|
||||
*/
|
||||
handleNavigationParams() {
|
||||
const {route, navigation} = this.props;
|
||||
if (route.params != null) {
|
||||
this.host = route.params.host;
|
||||
let {path} = route.params;
|
||||
const {title} = route.params;
|
||||
if (this.host != null && path != null) {
|
||||
path = path.replace(this.host, '');
|
||||
this.fullUrl = this.host + path;
|
||||
} else this.fullUrl = this.host;
|
||||
|
||||
if (title != null) navigation.setOptions({title});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let injectedJavascript = "";
|
||||
let customPadding = null;
|
||||
if (this.host != null && this.injectedJS[this.host] != null)
|
||||
injectedJavascript = this.injectedJS[this.host];
|
||||
if (this.host != null && this.customPaddingFunctions[this.host] != null)
|
||||
customPadding = this.customPaddingFunctions[this.host];
|
||||
|
||||
if (this.fullUrl != null) {
|
||||
return (
|
||||
<WebViewScreen
|
||||
{...this.props}
|
||||
url={this.fullUrl}
|
||||
customJS={injectedJavascript}
|
||||
customPaddingFunction={customPadding}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BasicLoadingScreen/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
let injectedJavascript = '';
|
||||
let customPadding = null;
|
||||
if (this.host != null && this.injectedJS[this.host] != null)
|
||||
injectedJavascript = this.injectedJS[this.host];
|
||||
if (this.host != null && this.customPaddingFunctions[this.host] != null)
|
||||
customPadding = this.customPaddingFunctions[this.host];
|
||||
|
||||
if (this.fullUrl != null) {
|
||||
return (
|
||||
<WebViewScreen
|
||||
navigation={navigation}
|
||||
url={this.fullUrl}
|
||||
customJS={injectedJavascript}
|
||||
customPaddingFunction={customPadding}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <BasicLoadingScreen />;
|
||||
}
|
||||
}
|
||||
|
||||
export default WebsiteScreen;
|
||||
|
|
|
|||
|
|
@ -1,70 +1,85 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
const speedOffset = 5;
|
||||
|
||||
type ListenerFunctionType = (shouldHide: boolean) => void;
|
||||
|
||||
export type OnScrollType = {
|
||||
nativeEvent: {
|
||||
contentInset: {bottom: number, left: number, right: number, top: number},
|
||||
contentOffset: {x: number, y: number},
|
||||
contentSize: {height: number, width: number},
|
||||
layoutMeasurement: {height: number, width: number},
|
||||
zoomScale: number,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Class used to detect when to show or hide a component based on scrolling
|
||||
*/
|
||||
export default class AutoHideHandler {
|
||||
lastOffset: number;
|
||||
|
||||
lastOffset: number;
|
||||
isHidden: boolean;
|
||||
isHidden: boolean;
|
||||
|
||||
listeners: Array<Function>;
|
||||
listeners: Array<ListenerFunctionType>;
|
||||
|
||||
constructor(startHidden: boolean) {
|
||||
this.listeners = [];
|
||||
this.isHidden = startHidden;
|
||||
constructor(startHidden: boolean) {
|
||||
this.listeners = [];
|
||||
this.isHidden = startHidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to the hide event
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
addListener(listener: (shouldHide: boolean) => void) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies every listener whether they should hide or show.
|
||||
*
|
||||
* @param shouldHide
|
||||
*/
|
||||
notifyListeners(shouldHide: boolean) {
|
||||
this.listeners.forEach((func: ListenerFunctionType) => {
|
||||
func(shouldHide);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be used on the onScroll animated component event.
|
||||
*
|
||||
* Detects if the current speed exceeds a threshold and notifies listeners to hide or show.
|
||||
*
|
||||
* The hide even is triggered when the user scrolls down, and the show event on scroll up.
|
||||
* This does not take into account the speed when the y coordinate is negative, to prevent hiding on over scroll.
|
||||
* (When scrolling up and hitting the top on ios for example)
|
||||
*
|
||||
* //TODO Known issue:
|
||||
* When refreshing a list with the pull down gesture on ios,
|
||||
* this can trigger the hide event as it scrolls down the list to show the refresh indicator.
|
||||
* Android shows the refresh indicator on top of the list so this is not an issue.
|
||||
*
|
||||
* @param event The scroll event generated by the animated component onScroll prop
|
||||
*/
|
||||
onScroll(event: OnScrollType) {
|
||||
const {nativeEvent} = event;
|
||||
const speed =
|
||||
nativeEvent.contentOffset.y < 0
|
||||
? 0
|
||||
: this.lastOffset - nativeEvent.contentOffset.y;
|
||||
if (speed < -speedOffset && !this.isHidden) {
|
||||
// Go down
|
||||
this.notifyListeners(true);
|
||||
this.isHidden = true;
|
||||
} else if (speed > speedOffset && this.isHidden) {
|
||||
// Go up
|
||||
this.notifyListeners(false);
|
||||
this.isHidden = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to the hide event
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
addListener(listener: Function) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies every listener whether they should hide or show.
|
||||
*
|
||||
* @param shouldHide
|
||||
*/
|
||||
notifyListeners(shouldHide: boolean) {
|
||||
for (let i = 0; i < this.listeners.length; i++) {
|
||||
this.listeners[i](shouldHide);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be used on the onScroll animated component event.
|
||||
*
|
||||
* Detects if the current speed exceeds a threshold and notifies listeners to hide or show.
|
||||
*
|
||||
* The hide even is triggered when the user scrolls down, and the show event on scroll up.
|
||||
* This does not take into account the speed when the y coordinate is negative, to prevent hiding on over scroll.
|
||||
* (When scrolling up and hitting the top on ios for example)
|
||||
*
|
||||
* //TODO Known issue:
|
||||
* When refreshing a list with the pull down gesture on ios,
|
||||
* this can trigger the hide event as it scrolls down the list to show the refresh indicator.
|
||||
* Android shows the refresh indicator on top of the list so this is not an issue.
|
||||
*
|
||||
* @param nativeEvent The scroll event generated by the animated component onScroll prop
|
||||
*/
|
||||
onScroll({nativeEvent}: Object) {
|
||||
const speed = nativeEvent.contentOffset.y < 0 ? 0 : this.lastOffset - nativeEvent.contentOffset.y;
|
||||
if (speed < -speedOffset && !this.isHidden) { // Go down
|
||||
this.notifyListeners(true);
|
||||
this.isHidden = true;
|
||||
} else if (speed > speedOffset && this.isHidden) { // Go up
|
||||
this.notifyListeners(false);
|
||||
this.isHidden = false;
|
||||
}
|
||||
this.lastOffset = nativeEvent.contentOffset.y;
|
||||
}
|
||||
|
||||
this.lastOffset = nativeEvent.contentOffset.y;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {useTheme} from "react-native-paper";
|
||||
import {createCollapsibleStack} from "react-navigation-collapsible";
|
||||
import StackNavigator, {StackNavigationOptions} from "@react-navigation/stack";
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import {createCollapsibleStack} from 'react-navigation-collapsible';
|
||||
import StackNavigator, {StackNavigationOptions} from '@react-navigation/stack';
|
||||
|
||||
/**
|
||||
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
|
||||
|
|
@ -22,32 +22,34 @@ import StackNavigator, {StackNavigationOptions} from "@react-navigation/stack";
|
|||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function createScreenCollapsibleStack(
|
||||
name: string,
|
||||
Stack: StackNavigator,
|
||||
component: React.ComponentType<any>,
|
||||
title: string,
|
||||
useNativeDriver?: boolean,
|
||||
options?: StackNavigationOptions,
|
||||
headerColor?: string) {
|
||||
const {colors} = useTheme();
|
||||
const screenOptions = options != null ? options : {};
|
||||
return createCollapsibleStack(
|
||||
<Stack.Screen
|
||||
name={name}
|
||||
component={component}
|
||||
options={{
|
||||
title: title,
|
||||
headerStyle: {
|
||||
backgroundColor: headerColor!=null ? headerColor :colors.surface,
|
||||
},
|
||||
...screenOptions,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: headerColor!=null ? headerColor :colors.surface,
|
||||
useNativeDriver: useNativeDriver != null ? useNativeDriver : true, // native driver does not work with webview
|
||||
}
|
||||
)
|
||||
name: string,
|
||||
Stack: StackNavigator,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
component: React.ComponentType<any>,
|
||||
title: string,
|
||||
useNativeDriver?: boolean,
|
||||
options?: StackNavigationOptions,
|
||||
headerColor?: string,
|
||||
): React.Node {
|
||||
const {colors} = useTheme();
|
||||
const screenOptions = options != null ? options : {};
|
||||
return createCollapsibleStack(
|
||||
<Stack.Screen
|
||||
name={name}
|
||||
component={component}
|
||||
options={{
|
||||
title,
|
||||
headerStyle: {
|
||||
backgroundColor: headerColor != null ? headerColor : colors.surface,
|
||||
},
|
||||
...screenOptions,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
collapsedColor: headerColor != null ? headerColor : colors.surface,
|
||||
useNativeDriver: useNativeDriver != null ? useNativeDriver : true, // native driver does not work with webview
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,6 +64,12 @@ export function createScreenCollapsibleStack(
|
|||
* @param title
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function getWebsiteStack(name: string, Stack: any, component: any, title: string) {
|
||||
return createScreenCollapsibleStack(name, Stack, component, title, false);
|
||||
export function getWebsiteStack(
|
||||
name: string,
|
||||
Stack: StackNavigator,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
component: React.ComponentType<any>,
|
||||
title: string,
|
||||
): React.Node {
|
||||
return createScreenCollapsibleStack(name, Stack, component, title, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import i18n from 'i18n-js';
|
||||
import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import DateManager from '../managers/DateManager';
|
||||
import type {CustomTheme} from '../managers/ThemeManager';
|
||||
import type {CustomThemeType} from '../managers/ThemeManager';
|
||||
import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen';
|
||||
|
||||
/**
|
||||
|
|
@ -161,7 +161,7 @@ export function getValidRange(
|
|||
*/
|
||||
export function generateMarkedDates(
|
||||
isSelection: boolean,
|
||||
theme: CustomTheme,
|
||||
theme: CustomThemeType,
|
||||
range: Array<string>,
|
||||
): MarkedDatesObjectType {
|
||||
const markedDates = {};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue