Compare commits

..

No commits in common. "c86281cbd2d1bab90ec7bd5beb43e63221aba6ad" and "547af66977b346f6e22469a5d710b5f27f5203a3" have entirely different histories.

105 changed files with 9359 additions and 10348 deletions

4
App.js
View file

@ -10,7 +10,7 @@ import {OverflowMenuProvider} from 'react-navigation-header-buttons';
import LocaleManager from './src/managers/LocaleManager'; import LocaleManager from './src/managers/LocaleManager';
import AsyncStorageManager from './src/managers/AsyncStorageManager'; import AsyncStorageManager from './src/managers/AsyncStorageManager';
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider'; import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
import type {CustomThemeType} from './src/managers/ThemeManager'; import type {CustomTheme} from './src/managers/ThemeManager';
import ThemeManager from './src/managers/ThemeManager'; import ThemeManager from './src/managers/ThemeManager';
import MainNavigator from './src/navigation/MainNavigator'; import MainNavigator from './src/navigation/MainNavigator';
import AprilFoolsManager from './src/managers/AprilFoolsManager'; import AprilFoolsManager from './src/managers/AprilFoolsManager';
@ -35,7 +35,7 @@ type StateType = {
showIntro: boolean, showIntro: boolean,
showUpdate: boolean, showUpdate: boolean,
showAprilFools: boolean, showAprilFools: boolean,
currentTheme: CustomThemeType | null, currentTheme: CustomTheme | null,
}; };
export default class App extends React.Component<null, StateType> { export default class App extends React.Component<null, StateType> {

View file

@ -1,217 +1,210 @@
/* eslint-disable */
import React from 'react';
import ConnectionManager from '../../src/managers/ConnectionManager';
import {ERROR_TYPE} from '../../src/utils/WebData';
jest.mock('react-native-keychain'); jest.mock('react-native-keychain');
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native import React from 'react';
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
const c = ConnectionManager.getInstance(); const c = ConnectionManager.getInstance();
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
test('isLoggedIn yes', () => { test('isLoggedIn yes', () => {
jest jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return 'token';
.mockImplementationOnce(() => {
return 'token';
}); });
return expect(c.isLoggedIn()).toBe(true); return expect(c.isLoggedIn()).toBe(true);
}); });
test('isLoggedIn no', () => { test('isLoggedIn no', () => {
jest jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return null;
.mockImplementationOnce(() => {
return null;
}); });
return expect(c.isLoggedIn()).toBe(false); return expect(c.isLoggedIn()).toBe(false);
}); });
test('connect bad credentials', () => { test("isConnectionResponseValid", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { let json = {
return Promise.resolve({ error: 0,
json: () => { data: {token: 'token'}
return { };
error: ERROR_TYPE.BAD_CREDENTIALS, expect(c.isConnectionResponseValid(json)).toBeTrue();
data: {}, json = {
}; error: 2,
}, data: {}
}); };
}); expect(c.isConnectionResponseValid(json)).toBeTrue();
return expect(c.connect('email', 'password')).rejects.toBe( json = {
ERROR_TYPE.BAD_CREDENTIALS, error: 0,
); data: {token: ''}
};
expect(c.isConnectionResponseValid(json)).toBeFalse();
json = {
error: 'prout',
data: {token: ''}
};
expect(c.isConnectionResponseValid(json)).toBeFalse();
}); });
test('connect good credentials', () => { test("connect bad credentials", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.SUCCESS, error: ERROR_TYPE.BAD_CREDENTIALS,
data: {token: 'token'}, data: {}
}; };
}, },
})
}); });
}); return expect(c.connect('email', 'password'))
jest .rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
.spyOn(ConnectionManager.prototype, 'saveLogin')
.mockImplementationOnce(() => {
return Promise.resolve(true);
});
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
}); });
test('connect good credentials no consent', () => { test("connect good credentials", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.NO_CONSENT, error: ERROR_TYPE.SUCCESS,
data: {}, data: {token: 'token'}
}; };
}, },
})
}); });
}); jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
return expect(c.connect('email', 'password')).rejects.toBe( return Promise.resolve(true);
ERROR_TYPE.NO_CONSENT, });
); return expect(c.connect('email', 'password')).resolves.toBeTruthy();
}); });
test('connect good credentials, fail save token', () => { test("connect good credentials no consent", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.SUCCESS, error: ERROR_TYPE.NO_CONSENT,
data: {token: 'token'}, data: {}
}; };
}, },
})
}); });
}); return expect(c.connect('email', 'password'))
jest .rejects.toBe(ERROR_TYPE.NO_CONSENT);
.spyOn(ConnectionManager.prototype, 'saveLogin')
.mockImplementationOnce(() => {
return Promise.reject(false);
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.UNKNOWN,
);
}); });
test('connect connection error', () => { test("connect good credentials, fail save token", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject(); return Promise.resolve({
}); json: () => {
return expect(c.connect('email', 'password')).rejects.toBe( return {
ERROR_TYPE.CONNECTION_ERROR, 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 bogus response 1', () => { test("connect connection error", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.reject();
json: () => {
return {
thing: true,
wrong: '',
};
},
}); });
}); return expect(c.connect('email', 'password'))
return expect(c.connect('email', 'password')).rejects.toBe( .rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
ERROR_TYPE.CONNECTION_ERROR,
);
}); });
test('authenticatedRequest success', () => { test("connect bogus response 1", () => {
jest jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return Promise.resolve({
.mockImplementationOnce(() => { json: () => {
return 'token'; return {
thing: true,
wrong: '',
}
},
})
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { return expect(c.connect('email', 'password'))
return Promise.resolve({ .rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
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 error wrong token', () => {
jest test("authenticatedRequest success", () => {
.spyOn(ConnectionManager.prototype, 'getToken') jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.mockImplementationOnce(() => { return 'token';
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.BAD_TOKEN, error: ERROR_TYPE.SUCCESS,
data: {}, data: {coucou: 'toi'}
}; };
}, },
})
}); });
}); return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
return expect( .resolves.toStrictEqual({coucou: 'toi'});
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
}); });
test('authenticatedRequest error bogus response', () => { test("authenticatedRequest error wrong token", () => {
jest jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return 'token';
.mockImplementationOnce(() => {
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.SUCCESS, error: ERROR_TYPE.BAD_TOKEN,
}; data: {}
}, };
},
})
}); });
}); return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
return expect( .rejects.toBe(ERROR_TYPE.BAD_TOKEN);
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
}); });
test('authenticatedRequest connection error', () => { test("authenticatedRequest error bogus response", () => {
jest jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return 'token';
.mockImplementationOnce(() => {
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject(); return Promise.resolve({
}); json: () => {
return expect( return {
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'), error: ERROR_TYPE.SUCCESS,
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR); };
},
})
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
}); });
test('authenticatedRequest error no token', () => { test("authenticatedRequest connection error", () => {
jest jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
.spyOn(ConnectionManager.prototype, 'getToken') return 'token';
.mockImplementationOnce(() => {
return null;
}); });
return expect( jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'), return Promise.reject()
).rejects.toBe(ERROR_TYPE.UNKNOWN); });
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);
}); });

View file

@ -1,345 +1,319 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import * as EquipmentBooking from '../../src/utils/EquipmentBooking'; import * as EquipmentBooking from "../../src/utils/EquipmentBooking";
import i18n from 'i18n-js'; import i18n from "i18n-js";
test('getISODate', () => { test('getISODate', () => {
let date = new Date('2020-03-05 12:00'); let date = new Date("2020-03-05 12:00");
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05'); expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
date = new Date('2020-03-05'); date = new Date("2020-03-05");
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05'); expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
date = new Date('2020-03-05 00:00'); // Treated as local time date = new Date("2020-03-05 00:00"); // Treated as local time
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-04'); // Treated as UTC expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC
}); });
test('getCurrentDay', () => { test('getCurrentDay', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime()); new Date('2020-01-14 14:50:35').getTime()
expect(EquipmentBooking.getCurrentDay().getTime()).toBe( );
new Date('2020-01-14').getTime(), expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime());
);
}); });
test('isEquipmentAvailable', () => { test('isEquipmentAvailable', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); new Date('2020-07-09').getTime()
let testDevice = { );
id: 1, let testDevice = {
name: 'Petit barbecue', id: 1,
caution: 100, name: "Petit barbecue",
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}], caution: 100,
}; booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); };
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}]; testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}]; testDevice.booked_at = [{begin: "2020-07-09", end: "2020-07-10"}];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-8'}, {begin: "2020-07-07", end: "2020-07-8"},
{begin: '2020-07-10', end: '2020-07-12'}, {begin: "2020-07-10", end: "2020-07-12"},
]; ];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue(); expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
}); });
test('getFirstEquipmentAvailability', () => { test('getFirstEquipmentAvailability', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); new Date('2020-07-09').getTime()
let testDevice = { );
id: 1, let testDevice = {
name: 'Petit barbecue', id: 1,
caution: 100, name: "Petit barbecue",
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}], caution: 100,
}; booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
expect( };
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(), expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime());
).toBe(new Date('2020-07-11').getTime()); testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}]; expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime());
expect( testDevice.booked_at = [
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(), {begin: "2020-07-07", end: "2020-07-09"},
).toBe(new Date('2020-07-10').getTime()); {begin: "2020-07-10", end: "2020-07-16"},
testDevice.booked_at = [ ];
{begin: '2020-07-07', end: '2020-07-09'}, expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime());
{begin: '2020-07-10', end: '2020-07-16'}, testDevice.booked_at = [
]; {begin: "2020-07-07", end: "2020-07-09"},
expect( {begin: "2020-07-10", end: "2020-07-12"},
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(), {begin: "2020-07-14", end: "2020-07-16"},
).toBe(new Date('2020-07-17').getTime()); ];
testDevice.booked_at = [ expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime());
{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', () => { test('getRelativeDateString', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); new Date('2020-07-09').getTime()
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => { );
const prefix = 'screens.equipment.'; jest.spyOn(i18n, 't')
if (translationString === prefix + 'otherYear') return '0'; .mockImplementation((translationString: string) => {
else if (translationString === prefix + 'otherMonth') return '1'; const prefix = "screens.equipment.";
else if (translationString === prefix + 'thisMonth') return '2'; if (translationString === prefix + "otherYear")
else if (translationString === prefix + 'tomorrow') return '3'; return "0";
else if (translationString === prefix + 'today') return '4'; else if (translationString === prefix + "otherMonth")
else return null; return "1";
}); else if (translationString === prefix + "thisMonth")
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe( return "2";
'4', else if (translationString === prefix + "tomorrow")
); return "3";
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe( else if (translationString === prefix + "today")
'3', return "4";
); else
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe( return null;
'2', }
); );
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe( expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4");
'2', 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-08-30'))).toBe( expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-30"))).toBe("2");
'1', 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('2020-11-10'))).toBe( expect(EquipmentBooking.getRelativeDateString(new Date("2021-11-10"))).toBe("0");
'1',
);
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe(
'0',
);
}); });
test('getValidRange', () => { test('getValidRange', () => {
let testDevice = { let testDevice = {
id: 1, id: 1,
name: 'Petit barbecue', name: "Petit barbecue",
caution: 100, caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}], booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
}; };
let start = new Date('2020-07-11'); let start = new Date("2020-07-11");
let end = new Date('2020-07-15'); let end = new Date("2020-07-15");
let result = [ let result = [
'2020-07-11', "2020-07-11",
'2020-07-12', "2020-07-12",
'2020-07-13', "2020-07-13",
'2020-07-14', "2020-07-14",
'2020-07-15', "2020-07-15",
]; ];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
result, testDevice.booked_at = [
); {begin: "2020-07-07", end: "2020-07-10"},
testDevice.booked_at = [ {begin: "2020-07-13", end: "2020-07-15"},
{begin: '2020-07-07', end: '2020-07-10'}, ];
{begin: '2020-07-13', end: '2020-07-15'}, result = [
]; "2020-07-11",
result = ['2020-07-11', '2020-07-12']; "2020-07-12",
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( ];
result, expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
);
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}]; testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"}];
result = ['2020-07-11']; result = ["2020-07-11"];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
result, testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},];
); result = [
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}]; "2020-07-13",
result = ['2020-07-13', '2020-07-14', '2020-07-15']; "2020-07-14",
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( "2020-07-15",
result, ];
); expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
start = new Date('2020-07-14'); start = new Date("2020-07-14");
end = new Date('2020-07-14'); end = new Date("2020-07-14");
result = ['2020-07-14']; result = [
expect( "2020-07-14",
EquipmentBooking.getValidRange(start, start, testDevice), ];
).toStrictEqual(result); expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result);
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
result, expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
);
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
result,
);
start = new Date('2020-07-14'); start = new Date("2020-07-14");
end = new Date('2020-07-17'); end = new Date("2020-07-17");
result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17']; result = [
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual( "2020-07-14",
result, "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'}]; testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}];
result = ['2020-07-14', '2020-07-15', '2020-07-16']; result = [
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( "2020-07-14",
result, "2020-07-15",
); "2020-07-16",
];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-12', end: '2020-07-13'}, {begin: "2020-07-12", end: "2020-07-13"},
{begin: '2020-07-15', end: '2020-07-20'}, {begin: "2020-07-15", end: "2020-07-20"},
]; ];
start = new Date('2020-07-11'); start = new Date("2020-07-11");
end = new Date('2020-07-23'); end = new Date("2020-07-23");
result = ['2020-07-21', '2020-07-22', '2020-07-23']; result = [
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( "2020-07-21",
result, "2020-07-22",
); "2020-07-23",
];
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
}); });
test('generateMarkedDates', () => { test('generateMarkedDates', () => {
let theme = { let theme = {
colors: { colors: {
primary: 'primary', primary: "primary",
danger: 'primary', danger: "primary",
textDisabled: 'primary', textDisabled: "primary",
}, }
}; }
let testDevice = { let testDevice = {
id: 1, id: 1,
name: 'Petit barbecue', name: "Petit barbecue",
caution: 100, caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}], booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
}; };
let start = new Date('2020-07-11'); let start = new Date("2020-07-11");
let end = new Date('2020-07-13'); let end = new Date("2020-07-13");
let range = EquipmentBooking.getValidRange(start, end, testDevice); let range = EquipmentBooking.getValidRange(start, end, testDevice);
let result = { let result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary, color: theme.colors.primary
}, },
'2020-07-12': { "2020-07-12": {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.danger, color: theme.colors.danger
}, },
'2020-07-13': { "2020-07-13": {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary, color: theme.colors.primary
}, },
}; };
expect( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range), result = {
).toStrictEqual(result); "2020-07-11": {
result = { startingDay: true,
'2020-07-11': { endingDay: false,
startingDay: true, color: theme.colors.textDisabled
endingDay: false, },
color: theme.colors.textDisabled, "2020-07-12": {
}, startingDay: false,
'2020-07-12': { endingDay: false,
startingDay: false, color: theme.colors.textDisabled
endingDay: false, },
color: theme.colors.textDisabled, "2020-07-13": {
}, startingDay: false,
'2020-07-13': { endingDay: true,
startingDay: false, color: theme.colors.textDisabled
endingDay: true, },
color: theme.colors.textDisabled, };
}, expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
}; result = {
expect( "2020-07-11": {
EquipmentBooking.generateMarkedDates(false, theme, range), startingDay: true,
).toStrictEqual(result); endingDay: false,
result = { color: theme.colors.textDisabled
'2020-07-11': { },
startingDay: true, "2020-07-12": {
endingDay: false, startingDay: false,
color: theme.colors.textDisabled, endingDay: false,
}, color: theme.colors.textDisabled
'2020-07-12': { },
startingDay: false, "2020-07-13": {
endingDay: false, startingDay: false,
color: theme.colors.textDisabled, endingDay: true,
}, color: theme.colors.textDisabled
'2020-07-13': { },
startingDay: false, };
endingDay: true, range = EquipmentBooking.getValidRange(end, start, testDevice);
color: theme.colors.textDisabled, expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
},
};
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'}]; testDevice.booked_at = [{begin: "2020-07-13", end: "2020-07-15"},];
result = { result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary, color: theme.colors.primary
}, },
'2020-07-12': { "2020-07-12": {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary, color: theme.colors.primary
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}]; testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"},];
result = { result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: true, endingDay: true,
color: theme.colors.primary, color: theme.colors.primary
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-12', end: '2020-07-13'}, {begin: "2020-07-12", end: "2020-07-13"},
{begin: '2020-07-15', end: '2020-07-20'}, {begin: "2020-07-15", end: "2020-07-20"},
]; ];
start = new Date('2020-07-11'); start = new Date("2020-07-11");
end = new Date('2020-07-23'); end = new Date("2020-07-23");
result = { result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: true, endingDay: true,
color: theme.colors.primary, color: theme.colors.primary
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
result = { result = {
'2020-07-21': { "2020-07-21": {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary, color: theme.colors.primary
}, },
'2020-07-22': { "2020-07-22": {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.danger, color: theme.colors.danger
}, },
'2020-07-23': { "2020-07-23": {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary, color: theme.colors.primary
}, },
}; };
range = EquipmentBooking.getValidRange(end, start, testDevice); range = EquipmentBooking.getValidRange(end, start, testDevice);
expect( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
}); });

View file

@ -1,222 +1,210 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import * as Planning from '../../src/utils/Planning'; import * as Planning from "../../src/utils/Planning";
test('isDescriptionEmpty', () => { test('isDescriptionEmpty', () => {
expect(Planning.isDescriptionEmpty('')).toBeTrue(); expect(Planning.isDescriptionEmpty("")).toBeTrue();
expect(Planning.isDescriptionEmpty(' ')).toBeTrue(); expect(Planning.isDescriptionEmpty(" ")).toBeTrue();
// noinspection CheckTagEmptyBody // noinspection CheckTagEmptyBody
expect(Planning.isDescriptionEmpty('<p></p>')).toBeTrue(); expect(Planning.isDescriptionEmpty("<p></p>")).toBeTrue();
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>")).toBeTrue();
expect(Planning.isDescriptionEmpty('<p><br></p><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><br><br></p>")).toBeTrue();
expect(Planning.isDescriptionEmpty('<p><br>')).toBeTrue(); expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue();
expect(Planning.isDescriptionEmpty(null)).toBeTrue(); expect(Planning.isDescriptionEmpty(null)).toBeTrue();
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue(); expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
expect(Planning.isDescriptionEmpty('coucou')).toBeFalse(); expect(Planning.isDescriptionEmpty("coucou")).toBeFalse();
expect(Planning.isDescriptionEmpty('<p>coucou</p>')).toBeFalse(); expect(Planning.isDescriptionEmpty("<p>coucou</p>")).toBeFalse();
}); });
test('isEventDateStringFormatValid', () => { test('isEventDateStringFormatValid', () => {
expect(Planning.isEventDateStringFormatValid('2020-03-21 09:00')).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")).toBeTrue();
expect( expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse();
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'), expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse();
).toBeFalse(); expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse();
expect( expect(Planning.isEventDateStringFormatValid("")).toBeFalse();
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'), expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
).toBeFalse(); expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
}); });
test('stringToDate', () => { test('stringToDate', () => {
let testDate = new Date(); let testDate = new Date();
expect(Planning.stringToDate(undefined)).toBeNull(); expect(Planning.stringToDate(undefined)).toBeNull();
expect(Planning.stringToDate('')).toBeNull(); expect(Planning.stringToDate("")).toBeNull();
expect(Planning.stringToDate('garbage')).toBeNull(); expect(Planning.stringToDate("garbage")).toBeNull();
expect(Planning.stringToDate('2020-03-21')).toBeNull(); expect(Planning.stringToDate("2020-03-21")).toBeNull();
expect(Planning.stringToDate('09:00:00')).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();
expect(Planning.stringToDate('2020-03-21 09:g0:')).toBeNull(); expect(Planning.stringToDate("2020-03-21 09:g0:")).toBeNull();
testDate.setFullYear(2020, 2, 21); testDate.setFullYear(2020, 2, 21);
testDate.setHours(9, 0, 0, 0); testDate.setHours(9, 0, 0, 0);
expect(Planning.stringToDate('2020-03-21 09:00')).toEqual(testDate); expect(Planning.stringToDate("2020-03-21 09:00")).toEqual(testDate);
testDate.setFullYear(2020, 0, 31); testDate.setFullYear(2020, 0, 31);
testDate.setHours(18, 30, 0, 0); testDate.setHours(18, 30, 0, 0);
expect(Planning.stringToDate('2020-01-31 18:30')).toEqual(testDate); expect(Planning.stringToDate("2020-01-31 18:30")).toEqual(testDate);
testDate.setFullYear(2020, 50, 50); testDate.setFullYear(2020, 50, 50);
testDate.setHours(65, 65, 0, 0); testDate.setHours(65, 65, 0, 0);
expect(Planning.stringToDate('2020-51-50 65:65')).toEqual(testDate); expect(Planning.stringToDate("2020-51-50 65:65")).toEqual(testDate);
}); });
test('getFormattedEventTime', () => { test('getFormattedEventTime', () => {
expect(Planning.getFormattedEventTime(null, null)).toBe('/ - /'); expect(Planning.getFormattedEventTime(null, null))
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /'); .toBe('/ - /');
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /'); expect(Planning.getFormattedEventTime(undefined, undefined))
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe( .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'), expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00"))
).toBe('09:00'); .toBe('09:00');
expect( expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00"))
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'), .toBe('09:00 - 23:59');
).toBe('09:00 - 23:59'); expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00"))
expect( .toBe('20:30 - 23:00');
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
).toBe('20:30 - 23:00');
}); });
test('getDateOnlyString', () => { test('getDateOnlyString', () => {
expect(Planning.getDateOnlyString('2020-03-21 09:00')).toBe('2020-03-21'); 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-15 09:00")).toBe("2021-12-15");
expect(Planning.getDateOnlyString('2021-12-o5 09:00')).toBeNull(); expect(Planning.getDateOnlyString("2021-12-o5 09:00")).toBeNull();
expect(Planning.getDateOnlyString('2021-12-15 09:')).toBeNull(); expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull();
expect(Planning.getDateOnlyString('2021-12-15')).toBeNull(); expect(Planning.getDateOnlyString("2021-12-15")).toBeNull();
expect(Planning.getDateOnlyString('garbage')).toBeNull(); expect(Planning.getDateOnlyString("garbage")).toBeNull();
}); });
test('isEventBefore', () => { test('isEventBefore', () => {
expect( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'), "2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue();
).toBeTrue(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue();
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'), expect(Planning.isEventBefore(
).toBeTrue(); "2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue();
expect( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'), "2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue();
).toBeTrue(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue();
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( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'), "2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse();
).toBeFalse(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse();
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'), expect(Planning.isEventBefore(
).toBeFalse(); "2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse();
expect( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'), "2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse();
).toBeFalse(); expect(Planning.isEventBefore(
expect( "2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse();
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'), expect(Planning.isEventBefore(
).toBeFalse(); "2020-03-30 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(
expect(Planning.isEventBefore(undefined, undefined)).toBeFalse(); "garbage", "2020-03-21 10:15")).toBeFalse();
expect(Planning.isEventBefore(
undefined, undefined)).toBeFalse();
}); });
test('dateToString', () => { test('dateToString', () => {
let testDate = new Date(); let testDate = new Date();
testDate.setFullYear(2020, 2, 21); testDate.setFullYear(2020, 2, 21);
testDate.setHours(9, 0, 0, 0); testDate.setHours(9, 0, 0, 0);
expect(Planning.dateToString(testDate)).toBe('2020-03-21 09:00'); expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00");
testDate.setFullYear(2021, 0, 12); testDate.setFullYear(2021, 0, 12);
testDate.setHours(9, 10, 0, 0); testDate.setHours(9, 10, 0, 0);
expect(Planning.dateToString(testDate)).toBe('2021-01-12 09:10'); expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10");
testDate.setFullYear(2022, 11, 31); testDate.setFullYear(2022, 11, 31);
testDate.setHours(9, 10, 15, 0); testDate.setHours(9, 10, 15, 0);
expect(Planning.dateToString(testDate)).toBe('2022-12-31 09:10'); expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10");
}); });
test('generateEmptyCalendar', () => { test('generateEmptyCalendar', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime()); new Date('2020-01-14T00:00:00.000Z').getTime()
let calendar = Planning.generateEmptyCalendar(1); );
expect(calendar).toHaveProperty('2020-01-14'); let calendar = Planning.generateEmptyCalendar(1);
expect(calendar).toHaveProperty('2020-01-20'); expect(calendar).toHaveProperty("2020-01-14");
expect(calendar).toHaveProperty('2020-02-10'); expect(calendar).toHaveProperty("2020-01-20");
expect(Object.keys(calendar).length).toBe(32); expect(calendar).toHaveProperty("2020-02-10");
calendar = Planning.generateEmptyCalendar(3); expect(Object.keys(calendar).length).toBe(32);
expect(calendar).toHaveProperty('2020-01-14'); calendar = Planning.generateEmptyCalendar(3);
expect(calendar).toHaveProperty('2020-01-20'); expect(calendar).toHaveProperty("2020-01-14");
expect(calendar).toHaveProperty('2020-02-10'); expect(calendar).toHaveProperty("2020-01-20");
expect(calendar).toHaveProperty('2020-02-14'); expect(calendar).toHaveProperty("2020-02-10");
expect(calendar).toHaveProperty('2020-03-20'); expect(calendar).toHaveProperty("2020-02-14");
expect(calendar).toHaveProperty('2020-04-12'); expect(calendar).toHaveProperty("2020-03-20");
expect(Object.keys(calendar).length).toBe(92); expect(calendar).toHaveProperty("2020-04-12");
expect(Object.keys(calendar).length).toBe(92);
}); });
test('pushEventInOrder', () => { test('pushEventInOrder', () => {
let eventArray = []; let eventArray = [];
let event1 = {date_begin: '2020-01-14 09:15'}; let event1 = {date_begin: "2020-01-14 09:15"};
Planning.pushEventInOrder(eventArray, event1); Planning.pushEventInOrder(eventArray, event1);
expect(eventArray.length).toBe(1); expect(eventArray.length).toBe(1);
expect(eventArray[0]).toBe(event1); expect(eventArray[0]).toBe(event1);
let event2 = {date_begin: '2020-01-14 10:15'}; let event2 = {date_begin: "2020-01-14 10:15"};
Planning.pushEventInOrder(eventArray, event2); Planning.pushEventInOrder(eventArray, event2);
expect(eventArray.length).toBe(2); expect(eventArray.length).toBe(2);
expect(eventArray[0]).toBe(event1); expect(eventArray[0]).toBe(event1);
expect(eventArray[1]).toBe(event2); expect(eventArray[1]).toBe(event2);
let event3 = {date_begin: '2020-01-14 10:15', title: 'garbage'}; let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"};
Planning.pushEventInOrder(eventArray, event3); Planning.pushEventInOrder(eventArray, event3);
expect(eventArray.length).toBe(3); expect(eventArray.length).toBe(3);
expect(eventArray[0]).toBe(event1); expect(eventArray[0]).toBe(event1);
expect(eventArray[1]).toBe(event2); expect(eventArray[1]).toBe(event2);
expect(eventArray[2]).toBe(event3); expect(eventArray[2]).toBe(event3);
let event4 = {date_begin: '2020-01-13 09:00'}; let event4 = {date_begin: "2020-01-13 09:00"};
Planning.pushEventInOrder(eventArray, event4); Planning.pushEventInOrder(eventArray, event4);
expect(eventArray.length).toBe(4); expect(eventArray.length).toBe(4);
expect(eventArray[0]).toBe(event4); expect(eventArray[0]).toBe(event4);
expect(eventArray[1]).toBe(event1); expect(eventArray[1]).toBe(event1);
expect(eventArray[2]).toBe(event2); expect(eventArray[2]).toBe(event2);
expect(eventArray[3]).toBe(event3); expect(eventArray[3]).toBe(event3);
}); });
test('generateEventAgenda', () => { test('generateEventAgenda', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime()); new Date('2020-01-14T00:00:00.000Z').getTime()
let eventList = [ );
{date_begin: '2020-01-14 09:15'}, let eventList = [
{date_begin: '2020-02-01 09:15'}, {date_begin: "2020-01-14 09:15"},
{date_begin: '2020-01-15 09:15'}, {date_begin: "2020-02-01 09:15"},
{date_begin: '2020-02-01 09:30'}, {date_begin: "2020-01-15 09:15"},
{date_begin: '2020-02-01 08:30'}, {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); const calendar = Planning.generateEventAgenda(eventList, 2);
expect(calendar['2020-01-14'][0]).toBe(eventList[0]); expect(calendar["2020-01-14"].length).toBe(1);
expect(calendar['2020-01-15'].length).toBe(1); expect(calendar["2020-01-14"][0]).toBe(eventList[0]);
expect(calendar['2020-01-15'][0]).toBe(eventList[2]); expect(calendar["2020-01-15"].length).toBe(1);
expect(calendar['2020-02-01'].length).toBe(3); expect(calendar["2020-01-15"][0]).toBe(eventList[2]);
expect(calendar['2020-02-01'][0]).toBe(eventList[4]); expect(calendar["2020-02-01"].length).toBe(3);
expect(calendar['2020-02-01'][1]).toBe(eventList[1]); expect(calendar["2020-02-01"][0]).toBe(eventList[4]);
expect(calendar['2020-02-01'][2]).toBe(eventList[3]); expect(calendar["2020-02-01"][1]).toBe(eventList[1]);
expect(calendar["2020-02-01"][2]).toBe(eventList[3]);
}); });
test('getCurrentDateString', () => { test('getCurrentDateString', () => {
jest.spyOn(Date, 'now').mockImplementation(() => { jest.spyOn(Date, 'now')
let date = new Date(); .mockImplementation(() => {
date.setFullYear(2020, 0, 14); let date = new Date();
date.setHours(15, 30, 54, 65); date.setFullYear(2020, 0, 14);
return date.getTime(); date.setHours(15, 30, 54, 65);
}); return date.getTime();
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30'); });
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
}); });

View file

@ -1,167 +1,142 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import { import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash";
getCleanedMachineWatched,
getMachineEndDate,
getMachineOfId,
isMachineWatched,
} from '../../src/utils/Proxiwash';
test('getMachineEndDate', () => { test('getMachineEndDate', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T15:00:00.000Z').getTime()); new Date('2020-01-14T15:00:00.000Z').getTime()
let expectDate = new Date('2020-01-14T15:00:00.000Z'); );
expectDate.setHours(23); let expectDate = new Date('2020-01-14T15:00:00.000Z');
expectDate.setMinutes(10); expectDate.setHours(23);
expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe( expectDate.setMinutes(10);
expectDate.getTime(), expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime());
);
expectDate.setHours(16); expectDate.setHours(16);
expectDate.setMinutes(30); expectDate.setMinutes(30);
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe( expect(getMachineEndDate({endTime: "16:30"}).getTime()).toBe(expectDate.getTime());
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 jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T23:00:00.000Z').getTime()); new Date('2020-01-14T23:00:00.000Z').getTime()
expectDate = new Date('2020-01-14T23:00:00.000Z'); );
expectDate.setHours(0); expectDate = new Date('2020-01-14T23:00:00.000Z');
expectDate.setMinutes(30); expectDate.setHours(0);
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe( expectDate.setMinutes(30);
expectDate.getTime(), expect(getMachineEndDate({endTime: "00:30"}).getTime()).toBe(expectDate.getTime());
);
}); });
test('isMachineWatched', () => { test('isMachineWatched', () => {
let machineList = [ let machineList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:30', endTime: "20:30",
}, },
]; ];
expect( expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue();
isMachineWatched({number: '0', endTime: '23:30'}, machineList), expect(isMachineWatched({number: "1", endTime: "20:30"}, machineList)).toBeTrue();
).toBeTrue(); expect(isMachineWatched({number: "3", endTime: "20:30"}, machineList)).toBeFalse();
expect( expect(isMachineWatched({number: "1", endTime: "23:30"}, machineList)).toBeFalse();
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', () => { test('getMachineOfId', () => {
let machineList = [ let machineList = [
{ {
number: '0', number: "0",
}, },
{ {
number: '1', number: "1",
}, },
]; ];
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'}); expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"});
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'}); expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"});
expect(getMachineOfId('3', machineList)).toBeNull(); expect(getMachineOfId("3", machineList)).toBeNull();
}); });
test('getCleanedMachineWatched', () => { test('getCleanedMachineWatched', () => {
let machineList = [ let machineList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:30', endTime: "20:30",
}, },
{ {
number: '2', number: "2",
endTime: '', endTime: "",
}, },
]; ];
let watchList = [ let watchList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:30', endTime: "20:30",
}, },
{ {
number: '2', number: "2",
endTime: '', endTime: "",
}, },
]; ];
let cleanedList = watchList; let cleanedList = watchList;
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual( expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
cleanedList,
);
watchList = [ watchList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:30', endTime: "20:30",
}, },
{ {
number: '2', number: "2",
endTime: '15:30', endTime: "15:30",
}, },
]; ];
cleanedList = [ cleanedList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:30', endTime: "20:30",
}, },
]; ];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual( expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
cleanedList,
);
watchList = [ watchList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
{ {
number: '1', number: "1",
endTime: '20:31', endTime: "20:31",
}, },
{ {
number: '3', number: "3",
endTime: '15:30', endTime: "15:30",
}, },
]; ];
cleanedList = [ cleanedList = [
{ {
number: '0', number: "0",
endTime: '23:30', endTime: "23:30",
}, },
]; ];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual( expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
cleanedList,
);
}); });

View file

@ -0,0 +1,45 @@
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();
});

View file

@ -1,47 +0,0 @@
/* 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();
});

View file

@ -6,5 +6,4 @@ import {AppRegistry} from 'react-native';
import App from './App'; import App from './App';
import {name as appName} from './app.json'; import {name as appName} from './app.json';
// eslint-disable-next-line flowtype/require-return-type
AppRegistry.registerComponent(appName, () => App); AppRegistry.registerComponent(appName, () => App);

View file

@ -6,13 +6,12 @@
*/ */
module.exports = { module.exports = {
transformer: { transformer: {
// eslint-disable-next-line flowtype/require-return-type getTransformOptions: async () => ({
getTransformOptions: async () => ({ transform: {
transform: { experimentalImportSupport: false,
experimentalImportSupport: false, inlineRequires: false,
inlineRequires: false, },
}, }),
}), },
},
}; };

View file

@ -4,10 +4,10 @@ import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Headline, withTheme} from 'react-native-paper'; import {Headline, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: CustomTheme,
}; };
class VoteNotAvailable extends React.Component<PropsType> { class VoteNotAvailable extends React.Component<PropsType> {

View file

@ -12,12 +12,12 @@ import {
import {FlatList, StyleSheet} from 'react-native'; import {FlatList, StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen'; import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
teams: Array<VoteTeamType>, teams: Array<VoteTeamType>,
dateEnd: string, dateEnd: string,
theme: CustomThemeType, theme: CustomTheme,
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View file

@ -9,14 +9,14 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
startDate: string | null, startDate: string | null,
justVoted: boolean, justVoted: boolean,
hasVoted: boolean, hasVoted: boolean,
isVoteRunning: boolean, isVoteRunning: boolean,
theme: CustomThemeType, theme: CustomTheme,
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View file

@ -5,10 +5,10 @@ import {View} from 'react-native';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import Collapsible from 'react-native-collapsible'; import Collapsible from 'react-native-collapsible';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: CustomTheme,
title: string, title: string,
subtitle?: string, subtitle?: string,
left?: () => React.Node, left?: () => React.Node,

View file

@ -7,14 +7,13 @@ import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
import type {OnScrollType} from '../../utils/AutoHideHandler';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
onPress: (action: string, data?: string) => void, onPress: (action: string, data?: string) => void,
seekAttention: boolean, seekAttention: boolean,
}; };
@ -95,7 +94,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
} }
}; };
onScroll = (event: OnScrollType) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };

View file

@ -2,7 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import withCollapsible from '../../utils/withCollapsible'; import {withCollapsible} from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
export type CollapsibleComponentPropsType = { export type CollapsibleComponentPropsType = {

View file

@ -5,11 +5,11 @@ import {List, withTheme} from 'react-native-paper';
import {View} from 'react-native'; import {View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; };
class ActionsDashBoardItem extends React.Component<PropsType> { class ActionsDashBoardItem extends React.Component<PropsType> {

View file

@ -10,12 +10,12 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
eventNumber: number, eventNumber: number,
clickAction: () => void, clickAction: () => void,
theme: CustomThemeType, theme: CustomTheme,
children?: React.Node, children?: React.Node,
}; };

View file

@ -4,13 +4,13 @@ import * as React from 'react';
import {Badge, TouchableRipple, withTheme} from 'react-native-paper'; import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native'; import {Dimensions, Image, View} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
image: string | null, image: string | null,
onPress: () => void | null, onPress: () => void | null,
badgeCount: number | null, badgeCount: number | null,
theme: CustomThemeType, theme: CustomTheme,
}; };
const AnimatableBadge = Animatable.createAnimatableComponent(Badge); const AnimatableBadge = Animatable.createAnimatableComponent(Badge);

View file

@ -7,14 +7,14 @@ import type {
ClubCategoryType, ClubCategoryType,
ClubType, ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen'; } from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
onPress: () => void, onPress: () => void,
categoryTranslator: (id: number) => ClubCategoryType, categoryTranslator: (id: number) => ClubCategoryType,
item: ClubType, item: ClubType,
height: number, height: number,
theme: CustomThemeType, theme: CustomTheme,
}; };
class ClubListItem extends React.Component<PropsType> { class ClubListItem extends React.Component<PropsType> {

View file

@ -10,13 +10,13 @@ import type {
ServiceCategoryType, ServiceCategoryType,
ServiceItemType, ServiceItemType,
} from '../../../managers/ServicesManager'; } from '../../../managers/ServicesManager';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
item: ServiceCategoryType, item: ServiceCategoryType,
activeDashboard: Array<string>, activeDashboard: Array<string>,
onPress: (service: ServiceItemType) => void, onPress: (service: ServiceItemType) => void,
theme: CustomThemeType, theme: CustomTheme,
}; };
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;

View file

@ -3,7 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import {Image} from 'react-native'; import {Image} from 'react-native';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
@ -11,7 +11,7 @@ type PropsType = {
isActive: boolean, isActive: boolean,
height: number, height: number,
onPress: () => void, onPress: () => void,
theme: CustomThemeType, theme: CustomTheme,
}; };
class DashboardEditItem extends React.Component<PropsType> { class DashboardEditItem extends React.Component<PropsType> {

View file

@ -3,13 +3,13 @@
import * as React from 'react'; import * as React from 'react';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native'; import {Dimensions, Image, View} from 'react-native';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
image: string, image: string,
isActive: boolean, isActive: boolean,
onPress: () => void, onPress: () => void,
theme: CustomThemeType, theme: CustomTheme,
}; };
/** /**

View file

@ -4,7 +4,7 @@ import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper'; import {Avatar, List, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen'; import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import { import {
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
@ -17,7 +17,7 @@ type PropsType = {
userDeviceRentDates: [string, string], userDeviceRentDates: [string, string],
item: DeviceType, item: DeviceType,
height: number, height: number,
theme: CustomThemeType, theme: CustomTheme,
}; };
class EquipmentListItem extends React.Component<PropsType> { class EquipmentListItem extends React.Component<PropsType> {

View file

@ -10,7 +10,7 @@ import type {
PlanexGroupType, PlanexGroupType,
PlanexGroupCategoryType, PlanexGroupCategoryType,
} from '../../../screens/Planex/GroupSelectionScreen'; } from '../../../screens/Planex/GroupSelectionScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
item: PlanexGroupCategoryType, item: PlanexGroupCategoryType,
@ -19,7 +19,7 @@ type PropsType = {
currentSearchString: string, currentSearchString: string,
favoriteNumber: number, favoriteNumber: number,
height: number, height: number,
theme: CustomThemeType, theme: CustomTheme,
}; };
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;

View file

@ -2,11 +2,11 @@
import * as React from 'react'; import * as React from 'react';
import {IconButton, List, withTheme} from 'react-native-paper'; import {IconButton, List, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen'; import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: CustomTheme,
onPress: () => void, onPress: () => void,
onStarPress: () => void, onStarPress: () => void,
item: PlanexGroupType, item: PlanexGroupType,

View file

@ -1,236 +1,194 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {Avatar, Caption, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
Avatar, import {StyleSheet, View} from "react-native";
Caption, import ProxiwashConstants from "../../../constants/ProxiwashConstants";
List, import i18n from "i18n-js";
ProgressBar, import AprilFoolsManager from "../../../managers/AprilFoolsManager";
Surface, import * as Animatable from "react-native-animatable";
Text, import type {CustomTheme} from "../../../managers/ThemeManager";
withTheme, import type {Machine} from "../../../screens/Proxiwash/ProxiwashScreen";
} 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 = { type Props = {
item: ProxiwashMachineType, item: Machine,
theme: CustomThemeType, theme: CustomTheme,
onPress: ( onPress: Function,
title: string, isWatched: boolean,
item: ProxiwashMachineType,
isDryer: boolean, isDryer: boolean,
) => void, height: number,
isWatched: boolean, }
isDryer: boolean,
height: number,
};
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon); 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 * Component used to display a proxiwash item, showing machine progression and state
*/ */
class ProxiwashListItem extends React.Component<PropsType> { class ProxiwashListItem extends React.Component<Props> {
stateColors: {[key: string]: string};
stateStrings: {[key: string]: string}; stateColors: Object;
stateStrings: Object;
title: string; title: string;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.stateColors = {}; this.stateColors = {};
this.stateStrings = {}; this.stateStrings = {};
this.updateStateStrings(); this.updateStateStrings();
let displayNumber = props.item.number; let displayNumber = props.item.number;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber( displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number));
parseInt(props.item.number, 10),
);
this.title = props.isDryer this.title = props.isDryer
? i18n.t('screens.proxiwash.dryer') ? i18n.t('screens.proxiwash.dryer')
: i18n.t('screens.proxiwash.washer'); : i18n.t('screens.proxiwash.washer');
this.title += `${displayNumber}`; this.title += ' n°' + displayNumber;
} }
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: Props): boolean {
const {props} = this; const props = this.props;
return ( return (nextProps.theme.dark !== props.theme.dark)
nextProps.theme.dark !== props.theme.dark || || (nextProps.item.state !== props.item.state)
nextProps.item.state !== props.item.state || || (nextProps.item.donePercent !== props.item.donePercent)
nextProps.item.donePercent !== props.item.donePercent || || (nextProps.isWatched !== props.isWatched);
nextProps.isWatched !== props.isWatched }
);
}
onListItemPress = () => { updateStateStrings() {
const {props} = this; this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.states.ready');
props.onPress(this.title, props.item, props.isDryer); 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');
}
updateStateStrings() { updateStateColors() {
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t( const colors = this.props.theme.colors;
'screens.proxiwash.states.ready', this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = colors.proxiwashReadyColor;
); this.stateColors[ProxiwashConstants.machineStates.RUNNING] = colors.proxiwashRunningColor;
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t( this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = colors.proxiwashRunningNotStartedColor;
'screens.proxiwash.states.running', this.stateColors[ProxiwashConstants.machineStates.FINISHED] = colors.proxiwashFinishedColor;
); this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
this.stateStrings[ this.stateColors[ProxiwashConstants.machineStates.ERROR] = colors.proxiwashErrorColor;
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] = colors.proxiwashUnknownColor;
] = 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',
);
}
updateStateColors() { onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer);
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(): React.Node { render() {
const {props} = this; const props = this.props;
const {colors} = props.theme; const colors = props.theme.colors;
const machineState = props.item.state; const machineState = props.item.state;
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING; const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE; const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
const description = isRunning const description = isRunning ? props.item.startTime + '/' + props.item.endTime : '';
? `${props.item.startTime}/${props.item.endTime}` const stateIcon = ProxiwashConstants.stateIcons[machineState];
: ''; const stateString = this.stateStrings[machineState];
const stateIcon = ProxiwashConstants.stateIcons[machineState]; const progress = isRunning
const stateString = this.stateStrings[machineState]; ? props.item.donePercent !== ''
let progress; ? parseFloat(props.item.donePercent) / 100
if (isRunning && props.item.donePercent !== '') : 0
progress = parseFloat(props.item.donePercent) / 100; : 1;
else if (isRunning) progress = 0;
else progress = 1;
const icon = props.isWatched ? ( const icon = props.isWatched
<AnimatedIcon ? <AnimatedIcon
icon="bell-ring" icon={'bell-ring'}
animation="rubberBand" animation={"rubberBand"}
useNativeDriver useNativeDriver
size={50} size={50}
color={colors.primary} color={colors.primary}
style={styles.icon} style={styles.icon}
/> />
) : ( : <AnimatedIcon
<AnimatedIcon icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} animation={isRunning ? "pulse" : undefined}
animation={isRunning ? 'pulse' : undefined} iterationCount={"infinite"}
iterationCount="infinite" easing={"linear"}
easing="linear" duration={1000}
duration={1000} useNativeDriver
useNativeDriver size={40}
size={40} color={colors.text}
color={colors.text} style={styles.icon}
style={styles.icon} />;
/> this.updateStateColors();
); return (
this.updateStateColors(); <Surface
return ( style={{
<Surface ...styles.container,
style={{ height: props.height,
...styles.container, borderRadius: 4,
height: props.height, }}
borderRadius: 4, >
}}> {
{!isReady ? ( !isReady
<ProgressBar ? <ProgressBar
style={{ style={{
...styles.progressBar, ...styles.progressBar,
height: props.height, height: props.height
}} }}
progress={progress} progress={progress}
color={this.stateColors[machineState]} color={this.stateColors[machineState]}
/> />
) : null} : null
<List.Item }
title={this.title} <List.Item
description={description} title={this.title}
style={{ description={description}
height: props.height, style={{
justifyContent: 'center', height: props.height,
}} justifyContent: 'center',
onPress={this.onListItemPress} }}
left={(): React.Node => icon} onPress={this.onListItemPress}
right={(): React.Node => ( left={() => icon}
<View style={{flexDirection: 'row'}}> right={() => (
<View style={{justifyContent: 'center'}}> <View style={{flexDirection: 'row',}}>
<Text <View style={{justifyContent: 'center',}}>
style={ <Text style={
machineState === ProxiwashConstants.machineStates.FINISHED machineState === ProxiwashConstants.machineStates.FINISHED ?
? {fontWeight: 'bold'} {fontWeight: 'bold',} : {}
: {} }
}> >
{stateString} {stateString}
</Text> </Text>
{machineState === ProxiwashConstants.machineStates.RUNNING ? ( {
<Caption>{props.item.remainingTime} min</Caption> machineState === ProxiwashConstants.machineStates.RUNNING
) : null} ? <Caption>{props.item.remainingTime} min</Caption>
</View> : null
<View style={{justifyContent: 'center'}}> }
<Avatar.Icon
icon={stateIcon} </View>
color={colors.text} <View style={{justifyContent: 'center',}}>
size={30} <Avatar.Icon
style={styles.icon} icon={stateIcon}
color={colors.text}
size={30}
style={styles.icon}
/>
</View>
</View>)}
/> />
</View> </Surface>
</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); export default withTheme(ProxiwashListItem);

View file

@ -1,72 +1,72 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper'; import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from "i18n-js";
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = { type Props = {
theme: CustomThemeType, title: string,
title: string, isDryer: boolean,
isDryer: boolean, nbAvailable: number,
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 * Component used to display a proxiwash item, showing machine progression and state
*/ */
class ProxiwashListItem extends React.Component<PropsType> { class ProxiwashListItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.nbAvailable !== props.nbAvailable
);
}
render(): React.Node { constructor(props) {
const {props} = this; super(props);
const subtitle = `${props.nbAvailable} ${ }
props.nbAvailable <= 1
? i18n.t('screens.proxiwash.numAvailable') shouldComponentUpdate(nextProps: Props) {
: i18n.t('screens.proxiwash.numAvailablePlural') return (nextProps.theme.dark !== this.props.theme.dark)
}`; || (nextProps.nbAvailable !== this.props.nbAvailable)
const iconColor = }
props.nbAvailable > 0
? props.theme.colors.success render() {
: props.theme.colors.primary; const props = this.props;
return ( const subtitle = props.nbAvailable + ' ' + (
<View style={styles.container}> (props.nbAvailable <= 1)
<Avatar.Icon ? i18n.t('screens.proxiwash.numAvailable')
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} : i18n.t('screens.proxiwash.numAvailablePlural'));
color={iconColor} const iconColor = props.nbAvailable > 0
style={styles.icon} ? this.props.theme.colors.success
/> : this.props.theme.colors.primary;
<View style={{justifyContent: 'center'}}> return (
<Text style={styles.text}>{props.title}</Text> <View style={styles.container}>
<Text style={{color: props.theme.colors.subtitle}}>{subtitle}</Text> <Avatar.Icon
</View> icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
</View> 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>
);
}
} }
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); export default withTheme(ProxiwashListItem);

View file

@ -1,269 +1,259 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import * as Animatable from 'react-native-animatable'; import * as Animatable from "react-native-animatable";
import {Image, TouchableWithoutFeedback, View} from 'react-native'; import {Image, TouchableWithoutFeedback, View} from "react-native";
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
export type AnimatableViewRefType = {current: null | Animatable.View}; type Props = {
style?: ViewStyle,
emotion: number,
animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null,
loopAnimation: Animatable.AnimatableProperties | null,
onPress?: (viewRef: AnimatableViewRef) => null,
onLongPress?: (viewRef: AnimatableViewRef) => null,
}
type PropsType = { type State = {
emotion?: number, currentEmotion: 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 = { export type AnimatableViewRef = {current: null | Animatable.View};
currentEmotion: number,
};
const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png'); const MASCOT_IMAGE = require("../../../assets/mascot/mascot.png");
const MASCOT_EYES_NORMAL = require('../../../assets/mascot/mascot_eyes_normal.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_GIRLY = require("../../../assets/mascot/mascot_eyes_girly.png");
const MASCOT_EYES_CUTE = require('../../../assets/mascot/mascot_eyes_cute.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_WINK = require("../../../assets/mascot/mascot_eyes_wink.png");
const MASCOT_EYES_HEART = require('../../../assets/mascot/mascot_eyes_heart.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_EYES_ANGRY = require("../../../assets/mascot/mascot_eyes_angry.png");
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png'); const MASCOT_GLASSES = require("../../../assets/mascot/mascot_glasses.png");
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png'); const MASCOT_SUNGLASSES = require("../../../assets/mascot/mascot_sunglasses.png");
export const EYE_STYLE = { export const EYE_STYLE = {
NORMAL: 0, NORMAL: 0,
GIRLY: 2, GIRLY: 2,
CUTE: 3, CUTE: 3,
WINK: 4, WINK: 4,
HEART: 5, HEART: 5,
ANGRY: 6, ANGRY: 6,
}; }
const GLASSES_STYLE = { const GLASSES_STYLE = {
NORMAL: 0, NORMAL: 0,
COOl: 1, COOl: 1
}; }
export const MASCOT_STYLE = { export const MASCOT_STYLE = {
NORMAL: 0, NORMAL: 0,
HAPPY: 1, HAPPY: 1,
GIRLY: 2, GIRLY: 2,
WINK: 3, WINK: 3,
CUTE: 4, CUTE: 4,
INTELLO: 5, INTELLO: 5,
LOVE: 6, LOVE: 6,
COOL: 7, COOL: 7,
ANGRY: 8, ANGRY: 8,
RANDOM: 999, RANDOM: 999,
}; };
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,
};
viewRef: AnimatableViewRefType; class Mascot extends React.Component<Props, State> {
eyeList: {[key: number]: number | string}; static defaultProps = {
animated: false,
glassesList: {[key: number]: number | string}; entryAnimation: {
useNativeDriver: true,
onPress: (viewRef: AnimatableViewRefType) => void; animation: "rubberBand",
duration: 2000,
onLongPress: (viewRef: AnimatableViewRefType) => void; },
loopAnimation: {
initialEmotion: number; useNativeDriver: true,
animation: "swing",
constructor(props: PropsType) { duration: 2000,
super(props); iterationDelay: 250,
this.viewRef = React.createRef(); iterationCount: "infinite",
this.eyeList = {}; },
this.glassesList = {}; clickAnimation: {
this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; useNativeDriver: true,
this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; animation: "rubberBand",
this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; duration: 2000,
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 (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;
}
getGlasses(style: number): React.Node {
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',
): 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));
} }
if (emotion === MASCOT_STYLE.INTELLO) { viewRef: AnimatableViewRef;
// Needs to have normal eyes behind the glasses eyeList: { [key: number]: number | string };
final.push(this.getGlasses(GLASSES_STYLE.NORMAL)); glassesList: { [key: number]: number | string };
}
final.push(<View key="container2" />);
return final;
}
render(): React.Node { onPress: (viewRef: AnimatableViewRef) => null;
const {props, state} = this; onLongPress: (viewRef: AnimatableViewRef) => null;
const entryAnimation = props.animated ? props.entryAnimation : null;
const loopAnimation = props.animated ? props.loopAnimation : null; initialEmotion: number;
return (
<Animatable.View constructor(props: Props) {
style={{ super(props);
aspectRatio: 1, this.viewRef = React.createRef();
...props.style, this.eyeList = {};
}} this.glassesList = {};
// eslint-disable-next-line react/jsx-props-no-spreading this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL;
{...entryAnimation}> this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY;
<TouchableWithoutFeedback this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE;
onPress={() => { this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK;
this.onPress(this.viewRef); this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART;
}} this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY;
onLongPress={() => {
this.onLongPress(this.viewRef); this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES;
}}> this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
<Animatable.View ref={this.viewRef}>
this.initialEmotion = this.props.emotion;
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
this.state = {
currentEmotion: this.initialEmotion
}
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 (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() {
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
return (
<Animatable.View <Animatable.View
// eslint-disable-next-line react/jsx-props-no-spreading
{...loopAnimation}>
<Image
source={MASCOT_IMAGE}
style={{ style={{
width: '100%', aspectRatio: 1,
height: '100%', ...this.props.style
}} }}
/> {...entryAnimation}
{this.getEyes(state.currentEmotion)} >
<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>
</Animatable.View> </Animatable.View>
</Animatable.View> );
</TouchableWithoutFeedback> }
</Animatable.View>
);
}
} }
export default Mascot; export default Mascot;

View file

@ -1,312 +1,283 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { import {Avatar, Button, Card, Paragraph, Portal, withTheme} from 'react-native-paper';
Avatar, import Mascot from "./Mascot";
Button, import * as Animatable from "react-native-animatable";
Card, import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
Paragraph, import type {CustomTheme} from "../../managers/ThemeManager";
Portal, import SpeechArrow from "./SpeechArrow";
withTheme, import AsyncStorageManager from "../../managers/AsyncStorageManager";
} 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 PropsType = { type Props = {
theme: CustomThemeType, theme: CustomTheme,
icon: string, icon: string,
title: string, title: string,
message: string, message: string,
buttons: { buttons: {
action: { action: {
message: string, message: string,
icon: string | null, icon: string | null,
color: string | null, color: string | null,
onPress?: () => void, onPress?: () => void,
},
cancel: {
message: string,
icon: string | null,
color: string | null,
onPress?: () => void,
}
}, },
cancel: { emotion: number,
message: string, visible?: boolean,
icon: string | null, prefKey?: string,
color: string | null, }
onPress?: () => void,
},
},
emotion: number,
visible?: boolean,
prefKey?: string,
};
type StateType = { type State = {
shouldRenderDialog: boolean, // Used to stop rendering after hide animation shouldRenderDialog: boolean, // Used to stop rendering after hide animation
dialogVisible: boolean, dialogVisible: boolean,
}; }
/** /**
* Component used to display a popup with the mascot. * Component used to display a popup with the mascot.
*/ */
class MascotPopup extends React.Component<PropsType, StateType> { class MascotPopup extends React.Component<Props, State> {
static defaultProps = {
visible: null,
prefKey: null,
};
mascotSize: number; mascotSize: number;
windowWidth: number;
windowHeight: number;
windowWidth: number; constructor(props: Props) {
super(props);
windowHeight: number; this.windowWidth = Dimensions.get('window').width;
this.windowHeight = Dimensions.get('window').height;
constructor(props: PropsType) { this.mascotSize = Dimensions.get('window').height / 6;
super(props);
this.windowWidth = Dimensions.get('window').width; if (this.props.visible != null) {
this.windowHeight = Dimensions.get('window').height; 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.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,
};
} }
}
componentDidMount(): * { onAnimationEnd = () => {
BackHandler.addEventListener( this.setState({
'hardwareBackPress', shouldRenderDialog: false,
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;
}
onAnimationEnd = () => { shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
this.setState({ if (nextProps.visible) {
shouldRenderDialog: false, this.state.shouldRenderDialog = true;
}); this.state.dialogVisible = true;
}; } else if (nextProps.visible !== this.props.visible
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
onBackButtonPressAndroid = (): boolean => { this.state.dialogVisible = false;
const {state, props} = this; setTimeout(this.onAnimationEnd, 300);
if (state.dialogVisible) { }
const {cancel} = props.buttons; return true;
const {action} = props.buttons;
if (cancel != null) this.onDismiss(cancel.onPress);
else this.onDismiss(action.onPress);
return true;
} }
return false;
};
getSpeechBubble(): React.Node { componentDidMount(): * {
const {state, props} = this; BackHandler.addEventListener(
return ( 'hardwareBackPress',
<Animatable.View this.onBackButtonPressAndroid
style={{ )
marginLeft: '10%', }
marginRight: '10%',
}} onBackButtonPressAndroid = () => {
useNativeDriver if (this.state.dialogVisible) {
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'} const cancel = this.props.buttons.cancel;
duration={state.dialogVisible ? 1000 : 300}> const action = this.props.buttons.action;
<SpeechArrow if (cancel != null)
style={{marginLeft: this.mascotSize / 3}} this.onDismiss(cancel.onPress);
size={20} else
color={props.theme.colors.mascotMessageArrow} this.onDismiss(action.onPress);
/> return true;
<Card } else {
style={{ return false;
borderColor: props.theme.colors.mascotMessageArrow, }
borderWidth: 4, };
borderRadius: 10,
}}> getSpeechBubble() {
<Card.Title return (
title={props.title} <Animatable.View
left={ style={{
props.icon != null marginLeft: "10%",
? (): React.Node => ( marginRight: "10%",
<Avatar.Icon }}
size={48} useNativeDriver={true}
style={{backgroundColor: 'transparent'}} animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
color={props.theme.colors.primary} duration={this.state.dialogVisible ? 1000 : 300}
icon={props.icon} >
<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}
/> />
)
: null
}
/>
<Card.Content
style={{
maxHeight: this.windowHeight / 3,
}}>
<ScrollView>
<Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
</ScrollView>
</Card.Content>
<Card.Actions style={{marginTop: 10, marginBottom: 10}}> <Card.Content style={{
{this.getButtons()} maxHeight: this.windowHeight / 3
</Card.Actions> }}>
</Card> <ScrollView>
</Animatable.View> <Paragraph style={{marginBottom: 10}}>
); {this.props.message}
} </Paragraph>
</ScrollView>
</Card.Content>
getMascot(): React.Node { <Card.Actions style={{marginTop: 10, marginBottom: 10}}>
const {props, state} = this; {this.getButtons()}
return ( </Card.Actions>
<Animatable.View </Card>
useNativeDriver </Animatable.View>
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 { getMascot() {
const {shouldRenderDialog} = this.state; return (
if (shouldRenderDialog) { <Animatable.View
return ( useNativeDriver={true}
<Portal> animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
{this.getBackground()} duration={this.state.dialogVisible ? 1500 : 200}
<View >
style={{ <Mascot
marginTop: 'auto', style={{width: this.mascotSize}}
marginBottom: 'auto', 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",
}}> }}>
<View {action != null
style={{ ? <Button
marginTop: -80, style={{
width: '100%', marginLeft: 'auto',
}}> marginRight: 'auto',
{this.getMascot()} marginBottom: 10,
{this.getSpeechBubble()} }}
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> </View>
</View> );
</Portal> }
);
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;
} }
return null;
}
} }
export default withTheme(MascotPopup); export default withTheme(MascotPopup);

View file

@ -1,43 +1,33 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type PropsType = { type Props = {
style?: ViewStyle | null, style?: ViewStyle,
size: number, size: number,
color: string, color: string,
}; }
export default class SpeechArrow extends React.Component<PropsType> { export default class SpeechArrow extends React.Component<Props> {
static defaultProps = {
style: null, render() {
}; return (
<View style={this.props.style}>
shouldComponentUpdate(): boolean { <View style={{
return false; width: 0,
} height: 0,
borderLeftWidth: 0,
render(): React.Node { borderRightWidth: this.props.size,
const {props} = this; borderBottomWidth: this.props.size,
return ( borderStyle: 'solid',
<View style={props.style}> backgroundColor: 'transparent',
<View borderLeftColor: 'transparent',
style={{ borderRightColor: 'transparent',
width: 0, borderBottomColor: this.props.color,
height: 0, }}/>
borderLeftWidth: 0, </View>
borderRightWidth: props.size, );
borderBottomWidth: props.size, }
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: props.color,
}}
/>
</View>
);
}
} }

View file

@ -1,63 +1,60 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {Agenda} from 'react-native-calendars'; import {Agenda} from "react-native-calendars";
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = { type Props = {
theme: CustomThemeType, theme: Object,
onRef: (ref: Agenda) => void, }
};
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class CustomAgenda extends React.Component<PropsType> { class CustomAgenda extends React.Component<Props> {
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,
}}
/>
);
}
render(): React.Node { getAgenda() {
const {props} = this; return <Agenda
// Completely recreate the component on theme change to force theme reload {...this.props}
if (props.theme.dark) ref={this.props.onRef}
return <View style={{flex: 1}}>{this.getAgenda()}</View>; theme={{
return this.getAgenda(); 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();
}
} }
export default withTheme(CustomAgenda); export default withTheme(CustomAgenda);

View file

@ -1,58 +1,47 @@
/* eslint-disable flowtype/require-parameter-type */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Text, withTheme} from 'react-native-paper'; import {Text, withTheme} from 'react-native-paper';
import HTML from 'react-native-render-html'; import HTML from "react-native-render-html";
import {Linking} from 'react-native'; import {Linking} from "react-native";
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = { type Props = {
theme: CustomThemeType, theme: Object,
html: string, html: string,
}; }
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class CustomHTML extends React.Component<PropsType> { class CustomHTML extends React.Component<Props> {
openWebLink = (event: {...}, link: string) => {
Linking.openURL(link);
};
getBasicText = ( openWebLink = (event, link) => {
htmlAttribs, Linking.openURL(link).catch((err) => console.error('Error opening link', err));
children, };
convertedCSSStyles,
passProps,
): React.Node => {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Text {...passProps}>{children}</Text>;
};
getListBullet = (): React.Node => { getBasicText = (htmlAttribs, children, convertedCSSStyles, passProps) => {
return <Text>- </Text>; return <Text {...passProps}>{children}</Text>;
}; };
render(): React.Node { getListBullet = (htmlAttribs, children, convertedCSSStyles, passProps) => {
const {props} = this; return (
// Surround description with p to allow text styling if the description is not html <Text>- </Text>
return ( );
<HTML };
html={`<p>${props.html}</p>`}
renderers={{ render() {
p: this.getBasicText, // Surround description with p to allow text styling if the description is not html
li: this.getBasicText, return <HTML
}} html={"<p>" + this.props.html + "</p>"}
listsPrefixesRenderers={{ renderers={{
ul: this.getListBullet, p: this.getBasicText,
}} li: this.getBasicText,
ignoredTags={['img']} }}
ignoredStyles={['color', 'background-color']} listsPrefixesRenderers={{
onLinkPress={this.openWebLink} ul: this.getListBullet
/> }}
); ignoredTags={['img']}
} ignoredStyles={['color', 'background-color']}
onLinkPress={this.openWebLink}/>;
}
} }
export default withTheme(CustomHTML); export default withTheme(CustomHTML);

View file

@ -1,39 +1,27 @@
// @flow // @flow
import * as React from 'react'; 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 {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: { const MaterialHeaderButton = (props: Object) =>
theme: CustomThemeType,
color: string,
}): React.Node => {
const {color, theme} = props;
return (
// $FlowFixMe
<HeaderButton <HeaderButton
// eslint-disable-next-line react/jsx-props-no-spreading {...props}
{...props} IconComponent={MaterialCommunityIcons}
IconComponent={MaterialCommunityIcons} iconSize={26}
iconSize={26} color={props.color != null ? props.color : props.theme.colors.text}
color={color != null ? color : theme.colors.text} />;
/>
); const MaterialHeaderButtons = (props: Object) => {
return (
<HeaderButtons
{...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/>
);
}; };
const MaterialHeaderButtons = (props: {...}): React.Node => { export default withTheme(MaterialHeaderButtons);
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'; export {Item} from 'react-navigation-header-buttons';

View file

@ -1,403 +1,411 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Platform, StatusBar, StyleSheet, View} from 'react-native'; import {Platform, StatusBar, StyleSheet, View} from "react-native";
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons'; import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AppIntroSlider from 'react-native-app-intro-slider'; import AppIntroSlider from "react-native-app-intro-slider";
import Update from "../../constants/Update";
import ThemeManager from "../../managers/ThemeManager";
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import * as Animatable from 'react-native-animatable'; import Mascot, {MASCOT_STYLE} from "../Mascot/Mascot";
import {Card} from 'react-native-paper'; import * as Animatable from "react-native-animatable";
import Update from '../../constants/Update'; import {Card} from "react-native-paper";
import ThemeManager from '../../managers/ThemeManager';
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
type PropsType = { type Props = {
onDone: () => void, onDone: Function,
isUpdate: boolean, isUpdate: boolean,
isAprilFools: boolean, isAprilFools: boolean,
}; };
type StateType = { type State = {
currentSlide: number, currentSlide: number,
}; }
type IntroSlideType = { type Slide = {
key: string, key: string,
title: string, title: string,
text: string, text: string,
view: () => React.Node, view: () => React.Node,
mascotStyle: number, mascotStyle: number,
colors: [string, string], 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 * Class used to create intro slides
*/ */
export default class CustomIntroSlider extends React.Component< export default class CustomIntroSlider extends React.Component<Props, State> {
PropsType,
StateType,
> {
sliderRef: {current: null | AppIntroSlider};
introSlides: Array<IntroSlideType>; state = {
currentSlide: 0,
updateSlides: Array<IntroSlideType>;
aprilFoolsSlides: Array<IntroSlideType>;
currentSlides: Array<IntroSlideType>;
/**
* Generates intro slides
*/
constructor() {
super();
this.state = {
currentSlide: 0,
};
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],
});
} }
this.aprilFoolsSlides = [ sliderRef: { current: null | AppIntroSlider };
{
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'],
},
];
}
/** introSlides: Array<Slide>;
* Render item to be used for the intro introSlides updateSlides: Array<Slide>;
* aprilFoolsSlides: Array<Slide>;
* @param item The item to be displayed currentSlides: Array<Slide>;
* @param dimensions Dimensions of the item
*/ /**
getIntroRenderItem = ({ * Generates intro slides
item, */
dimensions, constructor() {
}: { super();
item: IntroSlideType, this.sliderRef = React.createRef();
dimensions: {width: number, height: number}, this.introSlides = [
}): React.Node => { {
const {state} = this; key: '0', // Mascot
const index = parseInt(item.key, 10); title: i18n.t('intro.slideMain.title'),
return ( text: i18n.t('intro.slideMain.text'),
<LinearGradient view: this.getWelcomeView,
style={[styles.mainContent, dimensions]} mascotStyle: MASCOT_STYLE.NORMAL,
colors={item.colors} colors: ['#be1522', '#57080e'],
start={{x: 0, y: 0.1}} },
end={{x: 0.1, y: 1}}> {
{state.currentSlide === index ? ( key: '1',
<View style={{height: '100%', flex: 1}}> title: i18n.t('intro.slidePlanex.title'),
<View style={{flex: 1}}>{item.view()}</View> text: i18n.t('intro.slidePlanex.text'),
<Animatable.View animation="fadeIn"> view: () => this.getIconView("calendar-clock"),
{index !== 0 && index !== this.introSlides.length - 1 ? ( 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],
},
);
}
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 <Mascot
style={{ style={{
marginLeft: 30, ...styles.center,
marginBottom: 0, height: "80%"
width: 100, }}
marginTop: -30, emotion={MASCOT_STYLE.COOL}
}} animated={true}
emotion={item.mascotStyle} entryAnimation={{
animated animation: "slideInDown",
entryAnimation={{ duration: 2000,
animation: 'slideInLeft', }}
duration: 500, loopAnimation={{
}} animation: "pulse",
loopAnimation={{ duration: 2000,
animation: 'pulse', iterationCount: "infinite"
iterationCount: 'infinite', }}
duration: 2000,
}}
/> />
) : null} </View>
<View );
style={{ }
marginLeft: 50,
width: 0, getWelcomeView = () => {
height: 0, return (
borderLeftWidth: 20, <View style={{flex: 1}}>
borderRightWidth: 0, <Mascot
borderBottomWidth: 20, style={{
borderStyle: 'solid', ...styles.center,
backgroundColor: 'transparent', height: "80%"
borderLeftColor: 'transparent', }}
borderRightColor: 'transparent', emotion={MASCOT_STYLE.NORMAL}
borderBottomColor: 'rgba(0,0,0,0.60)', animated={true}
}} entryAnimation={{
/> animation: "bounceIn",
<Card duration: 2000,
style={{ }}
backgroundColor: 'rgba(0,0,0,0.38)', />
marginHorizontal: 20, <Animatable.Text
borderColor: 'rgba(0,0,0,0.60)', useNativeDriver={true}
borderWidth: 4, animation={"fadeInUp"}
borderRadius: 10, duration={500}
}}>
<Card.Content> style={{
<Animatable.Text color: "#fff",
animation="fadeIn" textAlign: "center",
delay={100} fontSize: 25,
style={styles.title}> }}>
{item.title} PABLO
</Animatable.Text> </Animatable.Text>
<Animatable.Text <Animatable.View
animation="fadeIn" useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
delay={200} delay={200}
style={styles.text}>
{item.text} style={{
</Animatable.Text> position: "absolute",
</Card.Content> bottom: 30,
</Card> 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});
};
onSkip = () => {
this.setStatusBarColor(this.currentSlides[this.currentSlides.length - 1].colors[0]);
if (this.sliderRef.current != null)
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
}
onDone = () => {
this.setStatusBarColor(ThemeManager.getCurrentTheme().colors.surface);
this.props.onDone();
}
renderNextButton = () => {
return (
<Animatable.View
animation={"fadeIn"}
style={{
borderRadius: 25,
padding: 5,
backgroundColor: "rgba(0,0,0,0.2)"
}}>
<MaterialCommunityIcons
name={"arrow-right"}
color={'#fff'}
size={40}/>
</Animatable.View> </Animatable.View>
</View> )
) : null} }
</LinearGradient>
);
};
getEndView = (): React.Node => { renderDoneButton = () => {
return ( return (
<View style={{flex: 1}}> <Animatable.View
<Mascot animation={"bounceIn"}
style={{
...styles.center,
height: '80%',
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
};
getWelcomeView = (): React.Node => { style={{
return ( borderRadius: 25,
<View style={{flex: 1}}> padding: 5,
<Mascot backgroundColor: "rgb(190,21,34)"
style={{ }}>
...styles.center, <MaterialCommunityIcons
height: '80%', name={"check"}
}} color={'#fff'}
emotion={MASCOT_STYLE.NORMAL} size={40}/>
animated </Animatable.View>
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>
);
};
static getIconView(icon: MaterialCommunityIconsGlyphs): React.Node { render() {
return ( this.currentSlides = this.introSlides;
<View style={{flex: 1}}> if (this.props.isUpdate)
<Animatable.View style={styles.center} animation="fadeIn"> this.currentSlides = this.updateSlides;
<MaterialCommunityIcons name={icon} color="#fff" size={200} /> else if (this.props.isAprilFools)
</Animatable.View> this.currentSlides = this.aprilFoolsSlides;
</View> this.setStatusBarColor(this.currentSlides[0].colors[0]);
); return (
} <AppIntroSlider
ref={this.sliderRef}
data={this.currentSlides}
extraData={this.state.currentSlide}
static setStatusBarColor(color: string) { renderItem={this.getIntroRenderItem}
if (Platform.OS === 'android') StatusBar.setBackgroundColor(color, true); renderNextButton={this.renderNextButton}
} renderDoneButton={this.renderDoneButton}
onSlideChange = (index: number) => { onDone={this.onDone}
CustomIntroSlider.setStatusBarColor(this.currentSlides[index].colors[0]); onSlideChange={this.onSlideChange}
this.setState({currentSlide: index}); onSkip={this.onSkip}
}; />
);
}
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',
},
});

View file

@ -2,10 +2,9 @@
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {Modalize} from 'react-native-modalize'; import {Modalize} from "react-native-modalize";
import {View} from 'react-native-animatable'; import {View} from "react-native-animatable";
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from "../Tabbar/CustomTabBar";
import type {CustomThemeType} from '../../managers/ThemeManager';
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -13,29 +12,25 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref. * @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*} * @return {*}
*/ */
function CustomModal(props: { function CustomModal(props) {
theme: CustomThemeType, const {colors} = props.theme;
onRef: (re: Modalize) => void, return (
children?: React.Node, <Modalize
}): React.Node { ref={props.onRef}
const {theme, onRef, children} = props; adjustToContentHeight
return ( handlePosition={'inside'}
<Modalize modalStyle={{backgroundColor: colors.card}}
ref={onRef} handleStyle={{backgroundColor: colors.primary}}
adjustToContentHeight >
handlePosition="inside" <View style={{
modalStyle={{backgroundColor: theme.colors.card}} paddingBottom: CustomTabBar.TAB_BAR_HEIGHT
handleStyle={{backgroundColor: theme.colors.primary}}> }}>
<View {props.children}
style={{ </View>
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
}}> </Modalize>
{children} );
</View>
</Modalize>
);
} }
CustomModal.defaultProps = {children: null};
export default withTheme(CustomModal); export default withTheme(CustomModal);

View file

@ -2,19 +2,19 @@
import * as React from 'react'; import * as React from 'react';
import {Text, withTheme} from 'react-native-paper'; import {Text, withTheme} from 'react-native-paper';
import {View} from 'react-native-animatable'; import {View} from "react-native-animatable";
import Slider, {SliderProps} from '@react-native-community/slider'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomThemeType} from '../../managers/ThemeManager'; import Slider, {SliderProps} from "@react-native-community/slider";
type PropsType = { type Props = {
theme: CustomThemeType, theme: CustomTheme,
valueSuffix?: string, valueSuffix: string,
...SliderProps, ...SliderProps
}; }
type StateType = { type State = {
currentValue: number, currentValue: number,
}; }
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -22,44 +22,37 @@ type StateType = {
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref. * @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*} * @return {*}
*/ */
class CustomSlider extends React.Component<PropsType, StateType> { class CustomSlider extends React.Component<Props, State> {
static defaultProps = {
valueSuffix: '',
};
constructor(props: PropsType) { static defaultProps = {
super(props); valueSuffix: "",
this.state = { }
currentValue: props.value,
};
}
onValueChange = (value: number) => { state = {
const {props} = this; currentValue: this.props.value,
this.setState({currentValue: value}); }
if (props.onValueChange != null) props.onValueChange(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>
);
}
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); export default withTheme(CustomSlider);

View file

@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper'; import {ActivityIndicator, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
/** /**
* Component used to display a header button * Component used to display a header button
@ -11,29 +10,28 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @param props Props to pass to the component * @param props Props to pass to the component
* @return {*} * @return {*}
*/ */
function BasicLoadingScreen(props: { function BasicLoadingScreen(props) {
theme: CustomThemeType, const {colors} = props.theme;
isAbsolute: boolean, let position = undefined;
}): React.Node { if (props.isAbsolute !== undefined && props.isAbsolute)
const {theme, isAbsolute} = props; position = 'absolute';
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
return ( return (
<View <View style={{
style={{ backgroundColor: colors.background,
backgroundColor: colors.background, position: position,
position, top: 0,
top: 0, right: 0,
right: 0, width: '100%',
width: '100%', height: '100%',
height: '100%', justifyContent: 'center',
justifyContent: 'center', }}>
}}> <ActivityIndicator
<ActivityIndicator animating size="large" color={colors.primary} /> animating={true}
</View> size={'large'}
); color={colors.primary}/>
</View>
);
} }
export default withTheme(BasicLoadingScreen); export default withTheme(BasicLoadingScreen);

View file

@ -2,192 +2,191 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Subheading, withTheme} from 'react-native-paper'; import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {ERROR_TYPE} from "../../utils/WebData";
import * as Animatable from 'react-native-animatable'; 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 PropsType = { type Props = {
navigation: StackNavigationProp, navigation: Object,
theme: CustomThemeType, route: Object,
route: {name: string}, errorCode: number,
onRefresh?: () => void, onRefresh: Function,
errorCode?: number, icon: string,
icon?: string, message: string,
message?: string, showRetryButton: boolean,
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',
},
});
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>
);
}
} }
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>
);
}
}
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',
}
});
export default withTheme(ErrorView); export default withTheme(ErrorView);

View file

@ -9,7 +9,7 @@ import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
import withCollapsible from '../../utils/withCollapsible'; import {withCollapsible} from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import {ERROR_TYPE, readData} from '../../utils/WebData'; import {ERROR_TYPE, readData} from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';

View file

@ -1,247 +1,233 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import WebView from 'react-native-webview'; import WebView from "react-native-webview";
import { import BasicLoadingScreen from "./BasicLoadingScreen";
Divider, import ErrorView from "./ErrorView";
HiddenItem, import {ERROR_TYPE} from "../../utils/WebData";
OverflowMenu,
} from 'react-navigation-header-buttons';
import i18n from 'i18n-js';
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 MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData'; import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons";
import ErrorView from './ErrorView'; import i18n from 'i18n-js';
import BasicLoadingScreen from './BasicLoadingScreen'; 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";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
url: string, url: string,
collapsibleStack: Collapsible, customJS: string,
onMessage: (event: {nativeEvent: {data: string}}) => void, customPaddingFunction: null | (padding: number) => string,
onScroll: (event: SyntheticEvent<EventTarget>) => void, collapsibleStack: Collapsible,
customJS?: string, onMessage: Function,
customPaddingFunction?: null | ((padding: number) => string), onScroll: Function,
showAdvancedControls?: boolean, showAdvancedControls: boolean,
}; }
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
/** /**
* Class defining a webview screen. * Class defining a webview screen.
*/ */
class WebViewScreen extends React.PureComponent<PropsType> { class WebViewScreen extends React.PureComponent<Props> {
static defaultProps = {
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
webviewRef: {current: null | WebView}; static defaultProps = {
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
canGoBack: boolean; webviewRef: { current: null | WebView };
constructor() { canGoBack: boolean;
super();
this.webviewRef = React.createRef();
this.canGoBack = false;
}
/** constructor() {
* Creates header buttons and listens to events after mounting super();
*/ this.webviewRef = React.createRef();
componentDidMount() { this.canGoBack = false;
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;
};
/** /**
* Gets header refresh and open in browser buttons * Creates header buttons and listens to events after mounting
* */
* @return {*} componentDidMount() {
*/ this.props.navigation.setOptions({
getBasicButton = (): React.Node => { headerRight: this.props.showAdvancedControls
return ( ? this.getAdvancedButtons
<MaterialHeaderButtons> : this.getBasicButton,
<Item });
title="refresh" this.props.navigation.addListener(
iconName="refresh" 'focus',
onPress={this.onRefreshClicked} () =>
/> BackHandler.addEventListener(
<Item 'hardwareBackPress',
title={i18n.t('general.openInBrowser')} this.onBackButtonPressAndroid
iconName="open-in-new" )
onPress={this.onOpenClicked} );
/> this.props.navigation.addListener(
</MaterialHeaderButtons> 'blur',
); () =>
}; BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/** /**
* Creates advanced header control buttons. * Goes back on the webview or on the navigation stack if we cannot go back anymore
* These buttons allows the user to refresh, go back, go forward and open in the browser. *
* * @returns {boolean}
* @returns {*} */
*/ onBackButtonPressAndroid = () => {
getAdvancedButtons = (): React.Node => { if (this.canGoBack) {
const {props} = this; this.onGoBackClicked();
return ( return true;
<MaterialHeaderButtons> }
<Item return false;
title="refresh" };
iconName="refresh"
onPress={this.onRefreshClicked} /**
/> * Gets header refresh and open in browser buttons
<OverflowMenu *
style={{marginHorizontal: 10}} * @return {*}
OverflowIcon={ */
<MaterialCommunityIcons getBasicButton = () => {
name="dots-vertical" return (
size={26} <MaterialHeaderButtons>
color={props.theme.colors.text} <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)}
/> />
}> );
<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)); export default withCollapsible(withTheme(WebViewScreen));

View file

@ -2,216 +2,179 @@
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import Animated from 'react-native-reanimated'; import TabIcon from "./TabIcon";
import {Collapsible} from 'react-navigation-collapsible'; import TabHomeIcon from "./TabHomeIcon";
import {StackNavigationProp} from '@react-navigation/stack'; import {Animated} from 'react-native';
import TabIcon from './TabIcon'; import {Collapsible} from "react-navigation-collapsible";
import TabHomeIcon from './TabHomeIcon';
import type {CustomThemeType} from '../../managers/ThemeManager';
type RouteType = { type Props = {
name: string, state: Object,
key: string, descriptors: Object,
params: {collapsible: Collapsible}, navigation: Object,
state: { theme: Object,
index: number, collapsibleStack: Object,
routes: Array<RouteType>, }
},
};
type PropsType = { type State = {
state: { translateY: AnimatedValue,
index: number, barSynced: boolean,
routes: Array<RouteType>, }
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomThemeType,
};
type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
};
const TAB_ICONS = { const TAB_ICONS = {
proxiwash: 'tshirt-crew', proxiwash: 'tshirt-crew',
services: 'account-circle', services: 'account-circle',
planning: 'calendar-range', planning: 'calendar-range',
planex: 'clock', planex: 'clock',
}; };
class CustomTabBar extends React.Component<PropsType, StateType> { class CustomTabBar extends React.Component<Props, State> {
static TAB_BAR_HEIGHT = 48;
constructor() { static TAB_BAR_HEIGHT = 48;
super();
this.state = {
translateY: new Animated.Value(0),
};
}
/** state = {
* Navigates to the given route if it is different from the current one translateY: new Animated.Value(0),
*
* @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 { syncTabBar = (route, index) => {
const {props} = this; const state = this.props.state;
return props.state.routes.map(this.getRenderIcon); 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
});
}
}
};
syncTabBar = (route: RouteType, index: number) => { /**
const {state} = this.props; * Navigates to the given route if it is different from the current one
const isFocused = state.index === index; *
if (isFocused) { * @param route Destination route
const stackState = route.state; * @param currentIndex The current route index
const stackRoute = * @param destIndex The destination route index
stackState != null ? stackState.routes[stackState.index] : null; */
const params: {collapsible: Collapsible} | null = onItemPress(route: Object, currentIndex: number, destIndex: number) {
stackRoute != null ? stackRoute.params : null; const event = this.props.navigation.emit({
const collapsible = params != null ? params.collapsible : null; type: 'tabPress',
if (collapsible != null) { target: route.key,
this.setState({ canPreventDefault: true,
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);
} }
};
render(): React.Node { /**
const {props, state} = this; * Navigates to tetris screen on home button long press
props.navigation.addListener('state', this.onRouteChange); *
const icons = this.getIcons(); * @param route
// $FlowFixMe */
return ( onItemLongPress(route: Object) {
<Animated.View const event = this.props.navigation.emit({
useNativeDriver type: 'tabLongPress',
style={{ target: route.key,
flexDirection: 'row', canPreventDefault: true,
height: CustomTabBar.TAB_BAR_HEIGHT, });
width: '100%', if (route.name === "home" && !event.defaultPrevented)
position: 'absolute', this.props.navigation.navigate('game-start');
bottom: 0, }
left: 0,
backgroundColor: props.theme.colors.surface, /**
transform: [{translateY: state.translateY}], * Gets an icon for the given route if it is not the home one as it uses a custom button
}}> *
{icons} * @param route
</Animated.View> * @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>
);
}
} }
export default withTheme(CustomTabBar); export default withTheme(CustomTabBar);

View file

@ -1,133 +1,106 @@
// @flow // @flow
import * as React from 'react'; 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 {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 PropsType = { type Props = {
focused: boolean, focused: boolean,
onPress: () => void, onPress: Function,
onLongPress: () => void, onLongPress: Function,
theme: CustomThemeType, theme: Object,
tabBarHeight: number, tabBarHeight: number,
}; }
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabHomeIcon extends React.Component<PropsType> { class TabHomeIcon extends React.Component<Props> {
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,
},
},
});
}
shouldComponentUpdate(nextProps: PropsType): boolean { focusedIcon = require('../../../assets/tab-icon.png');
const {focused} = this.props; unFocusedIcon = require('../../../assets/tab-icon-outline.png');
return nextProps.focused !== focused;
}
getIconRender = ({ constructor(props) {
size, super(props);
color, Animatable.initializeRegistryWithDefinitions({
}: { fabFocusIn: {
size: number, "0": {
color: string, scale: 1, translateY: 0
}): React.Node => { },
const {focused} = this.props; "0.9": {
if (focused) scale: 1.2, translateY: -9
return ( },
<Image "1": {
source={FOCUSED_ICON} scale: 1.1, translateY: -7
style={{ },
width: size, },
height: size, fabFocusOut: {
tintColor: color, "0": {
}} scale: 1.1, translateY: -6
/> },
); "1": {
return ( scale: 1, translateY: 0
<Image },
source={UNFOCUSED_ICON} }
style={{ });
width: size, }
height: size,
tintColor: color, 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>
);
}
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);

View file

@ -1,117 +1,114 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons'; import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import * as Animatable from 'react-native-animatable'; import * as Animatable from "react-native-animatable";
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = {
focused: boolean,
color: string,
label: string,
icon: MaterialCommunityIconsGlyphs,
onPress: Function,
onLongPress: Function,
theme: Object,
extraData: any,
}
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 * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabIcon extends React.Component<PropsType> { class TabIcon extends React.Component<Props> {
firstRender: boolean;
constructor(props: PropsType) { firstRender: boolean;
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() { constructor(props) {
this.firstRender = false; 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;
}
shouldComponentUpdate(nextProps: PropsType): boolean { componentDidMount() {
const {props} = this; this.firstRender = false;
return ( }
nextProps.focused !== props.focused ||
nextProps.theme.dark !== props.theme.dark ||
nextProps.extraData !== props.extraData
);
}
render(): React.Node { shouldComponentUpdate(nextProps: Props): boolean {
const {props} = this; return (nextProps.focused !== this.props.focused)
return ( || (nextProps.theme.dark !== this.props.theme.dark)
<TouchableRipple || (nextProps.extraData !== this.props.extraData);
onPress={props.onPress} }
onLongPress={props.onLongPress}
borderless render(): React$Node {
rippleColor={props.theme.colors.primary} const props = this.props;
style={{ return (
flex: 1, <TouchableRipple
justifyContent: 'center', onPress={props.onPress}
}}> onLongPress={props.onLongPress}
<View> borderless={true}
<Animatable.View rippleColor={this.props.theme.colors.primary}
duration={200} style={{
easing="ease-out" flex: 1,
animation={props.focused ? 'focusIn' : 'focusOut'} justifyContent: 'center',
useNativeDriver> }}
<MaterialCommunityIcons >
name={props.icon} <View>
color={props.color} <Animatable.View
size={26} duration={200}
style={{ easing={"ease-out"}
marginLeft: 'auto', animation={props.focused ? "focusIn" : "focusOut"}
marginRight: 'auto', useNativeDriver
}} >
/> <MaterialCommunityIcons
</Animatable.View> name={props.icon}
<Animatable.Text color={props.color}
animation={props.focused ? 'fadeOutDown' : 'fadeIn'} size={26}
useNativeDriver style={{
style={{ marginLeft: 'auto',
color: props.color, marginRight: 'auto',
marginLeft: 'auto', }}
marginRight: 'auto', />
fontSize: 10, </Animatable.View>
}}> <Animatable.Text
{props.label} animation={props.focused ? "fadeOutDown" : "fadeIn"}
</Animatable.Text> useNativeDriver
</View>
</TouchableRipple> style={{
); color: props.color,
} marginLeft: 'auto',
marginRight: 'auto',
fontSize: 10,
}}
>
{props.label}
</Animatable.Text>
</View>
</TouchableRipple>
);
}
} }
export default withTheme(TabIcon); export default withTheme(TabIcon);

View file

@ -1,13 +1,13 @@
export default { export default {
websites: { websites: {
AMICALE: 'https://www.amicale-insat.fr/', AMICALE: "https://www.amicale-insat.fr/",
AVAILABLE_ROOMS: 'http://planex.insa-toulouse.fr/salles.php', AVAILABLE_ROOMS: "http://planex.insa-toulouse.fr/salles.php",
BIB: 'https://bibbox.insa-toulouse.fr/', BIB: "https://bibbox.insa-toulouse.fr/",
BLUEMIND: 'https://etud-mel.insa-toulouse.fr/webmail/', BLUEMIND: "https://etud-mel.insa-toulouse.fr/webmail/",
ELUS_ETUDIANTS: 'https://etud.insa-toulouse.fr/~eeinsat/', ELUS_ETUDIANTS: "https://etud.insa-toulouse.fr/~eeinsat/",
ENT: 'https://ent.insa-toulouse.fr/', ENT: "https://ent.insa-toulouse.fr/",
INSA_ACCOUNT: 'https://moncompte.insa-toulouse.fr/', INSA_ACCOUNT: "https://moncompte.insa-toulouse.fr/",
TUTOR_INSA: 'https://www.etud.insa-toulouse.fr/~tutorinsa/', TUTOR_INSA: "https://www.etud.insa-toulouse.fr/~tutorinsa/",
WIKETUD: 'https://wiki.etud.insa-toulouse.fr/', WIKETUD: "https://wiki.etud.insa-toulouse.fr/",
}, },
}; }

View file

@ -1,20 +1,20 @@
export default { export default {
machineStates: { machineStates: {
AVAILABLE: 0, "AVAILABLE": 0,
RUNNING: 1, "RUNNING": 1,
RUNNING_NOT_STARTED: 2, "RUNNING_NOT_STARTED": 2,
FINISHED: 3, "FINISHED": 3,
UNAVAILABLE: 4, "UNAVAILABLE": 4,
ERROR: 5, "ERROR": 5,
UNKNOWN: 6, "UNKNOWN": 6,
}, },
stateIcons: { stateIcons: {
0: 'radiobox-blank', 0: 'radiobox-blank',
1: 'progress-check', 1: 'progress-check',
2: 'alert-circle-outline', 2: 'alert-circle-outline',
3: 'check-circle', 3: 'check-circle',
4: 'alert-octagram-outline', 4: 'alert-octagram-outline',
5: 'alert', 5: 'alert',
6: 'help-circle-outline', 6: 'help-circle-outline',
}, }
}; };

View file

@ -1,6 +1,6 @@
// @flow // @flow
import i18n from 'i18n-js'; import i18n from "i18n-js";
/** /**
* Singleton used to manage update slides. * Singleton used to manage update slides.
@ -14,47 +14,51 @@ import i18n from 'i18n-js';
* </ul> * </ul>
*/ */
export default class Update { export default class Update {
// Increment the number to show the update slide
static number = 6;
// Change the number of slides to display // Increment the number to show the update slide
static slidesNumber = 4; 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 icons to be displayed on the update slide static instance: Update | null = null;
static iconList = ['star', 'clock', 'qrcode-scan', 'account'];
static colorsList = [ titleList: Array<string>;
['#e01928', '#be1522'], descriptionList: Array<string>;
['#7c33ec', '#5e11d1'],
['#337aec', '#114ed1'],
['#e01928', '#be1522'],
];
static instance: Update | null = null; /**
* Init translations
titleList: Array<string>; */
constructor() {
descriptionList: Array<string>; this.titleList = [];
this.descriptionList = [];
/** for (let i = 0; i < Update.slidesNumber; i++) {
* Init translations this.titleList.push(i18n.t('intro.updateSlide' + i + '.title'))
*/ this.descriptionList.push(i18n.t('intro.updateSlide' + i + '.text'))
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 * Get this class instance or create one if none is found
* *
* @returns {Update} * @returns {Update}
*/ */
static getInstance(): Update { static getInstance(): Update {
if (Update.instance == null) Update.instance = new Update(); return Update.instance === null ?
return Update.instance; Update.instance = new Update() :
} Update.instance;
} }
};

View file

@ -1,138 +1,130 @@
// @flow // @flow
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen'; import type {Machine} from "../screens/Proxiwash/ProxiwashScreen";
import type {CustomThemeType} from './ThemeManager';
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
/** /**
* Singleton class used to manage april fools * Singleton class used to manage april fools
*/ */
export default class AprilFoolsManager { export default class AprilFoolsManager {
static instance: AprilFoolsManager | null = null;
static fakeMachineNumber = [ static instance: AprilFoolsManager | null = null;
'', static fakeMachineNumber = [
'cos(ln(1))', "",
'0,5⁻¹', "cos(ln(1))",
'567/189', "0,5⁻¹",
'√2×√8', "567/189",
'√50×sin(9π/4)', "√2×√8",
'⌈π+e⌉', "√50×sin(9π/4)",
'div(rot(B))+7', "⌈π+e⌉",
'4×cosh(0)+4', "div(rot(B))+7",
'8-(-i)²', "4×cosh(0)+4",
'|5√2+5√2i|', "8-(-i)²",
'1×10¹+1×10⁰', "|5√2+5√2i|",
'Re(√192e^(iπ/6))', "1×10¹+1×10⁰",
]; "Re(√192e^(iπ/6))",
];
aprilFoolsEnabled: boolean;
aprilFoolsEnabled: boolean; constructor() {
let today = new Date();
constructor() { this.aprilFoolsEnabled = (today.getDate() === 1 && today.getMonth() === 3);
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);
} }
}
/** /**
* Changes proxiwash washers order * Get this class instance or create one if none is found
* * @returns {ThemeManager}
* @param washers */
*/ static getInstance(): AprilFoolsManager {
static getNewProxiwashWasherOrderedList( return AprilFoolsManager.instance === null ?
washers: Array<ProxiwashMachineType> | null, AprilFoolsManager.instance = new AprilFoolsManager() :
) { AprilFoolsManager.instance;
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);
} }
}
/** /**
* Gets the new display number for the given machine number * Adds fake menu entries
* *
* @param number * @param menu
* @returns {string} * @returns {Object}
*/ */
static getProxiwashMachineDisplayNumber(number: number): string { static getFakeMenuItem(menu: Array<{dishes: Array<{name: string}>}>) {
return AprilFoolsManager.fakeMachineNumber[number]; 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 and ugly april fools theme * Changes proxiwash dryers order
* *
* @param currentTheme * @param dryers
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}} */
*/ static getNewProxiwashDryerOrderedList(dryers: Array<Machine> | null) {
static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType { if (dryers != null) {
return { let second = dryers[1];
...currentTheme, dryers.splice(1, 1);
colors: { dryers.push(second);
...currentTheme.colors, }
primary: '#00be45', }
accent: '#00be45',
background: '#d02eee',
tabIcon: '#380d43',
card: '#eed639',
surface: '#eed639',
dividerBackground: '#c72ce4',
textDisabled: '#b9b9b9',
// Calendar/Agenda /**
agendaBackgroundColor: '#c72ce4', * Changes proxiwash washers order
agendaDayTextColor: '#6d6d6d', *
}, * @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);
}
}
isAprilFoolsEnabled(): boolean { /**
return this.aprilFoolsEnabled; * 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;
}
};

View file

@ -1,7 +1,7 @@
// @flow // @flow
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import {SERVICES_KEY} from './ServicesManager'; import {SERVICES_KEY} from "./ServicesManager";
/** /**
* Singleton used to manage preferences. * Singleton used to manage preferences.
@ -10,232 +10,227 @@ import {SERVICES_KEY} from './ServicesManager';
*/ */
export default class AsyncStorageManager { export default class AsyncStorageManager {
static instance: AsyncStorageManager | null = null;
static PREFERENCES = { static instance: AsyncStorageManager | null = null;
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}; static PREFERENCES = {
debugUnlocked: {
constructor() { key: 'debugUnlocked',
this.#currentPreferences = {}; default: '0',
} },
showIntro: {
/** key: 'showIntro',
* Get this class instance or create one if none is found default: '1',
* @returns {AsyncStorageManager} },
*/ updateNumber: {
static getInstance(): AsyncStorageManager { key: 'updateNumber',
if (AsyncStorageManager.instance == null) default: '0',
AsyncStorageManager.instance = new AsyncStorageManager(); },
return AsyncStorageManager.instance; proxiwashNotifications: {
} key: 'proxiwashNotifications',
default: '5',
/** },
* Saves the value associated to the given key to preferences. nightModeFollowSystem: {
* key: 'nightModeFollowSystem',
* @param key default: '1',
* @param value },
*/ nightMode: {
static set( key: 'nightMode',
key: string, default: '1',
// eslint-disable-next-line flowtype/no-weak-types },
value: number | string | boolean | {...} | Array<any>, defaultStartScreen: {
) { key: 'defaultStartScreen',
AsyncStorageManager.getInstance().setPreference(key, value); default: 'home',
} },
servicesShowBanner: {
/** key: 'servicesShowBanner',
* Gets the string value of the given preference default: '1',
* },
* @param key proxiwashShowBanner: {
* @returns {string} key: 'proxiwashShowBanner',
*/ default: '1',
static getString(key: string): string { },
const value = AsyncStorageManager.getInstance().getPreference(key); homeShowBanner: {
return value != null ? value : ''; key: 'homeShowBanner',
} default: '1',
},
/** eventsShowBanner: {
* Gets the boolean value of the given preference key: 'eventsShowBanner',
* default: '1',
* @param key },
* @returns {boolean} planexShowBanner: {
*/ key: 'planexShowBanner',
static getBool(key: string): boolean { default: '1',
const value = AsyncStorageManager.getString(key); },
return value === '1' || value === 'true'; loginShowBanner: {
} key: 'loginShowBanner',
default: '1',
/** },
* Gets the number value of the given preference voteShowBanner: {
* key: 'voteShowBanner',
* @param key default: '1',
* @returns {number} },
*/ equipmentShowBanner: {
static getNumber(key: string): number { key: 'equipmentShowBanner',
return parseFloat(AsyncStorageManager.getString(key)); default: '1',
} },
gameStartShowBanner: {
/** key: 'gameStartShowBanner',
* Gets the object value of the given preference default: '1',
* },
* @param key proxiwashWatchedMachines: {
* @returns {{...}} key: 'proxiwashWatchedMachines',
*/ default: '[]',
// eslint-disable-next-line flowtype/no-weak-types },
static getObject(key: string): any { showAprilFoolsStart: {
return JSON.parse(AsyncStorageManager.getString(key)); key: 'showAprilFoolsStart',
} default: '1',
},
/** planexCurrentGroup: {
* Set preferences object current values from AsyncStorage. key: 'planexCurrentGroup',
* This function should be called at the app's start. default: '',
* },
* @return {Promise<void>} planexFavoriteGroups: {
*/ key: 'planexFavoriteGroups',
async loadPreferences() { default: '[]',
const prefKeys = []; },
// Get all available keys dashboardItems: {
Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => { key: 'dashboardItems',
prefKeys.push(key); default: JSON.stringify([
}); SERVICES_KEY.EMAIL,
// Get corresponding values SERVICES_KEY.WASHERS,
const resultArray = await AsyncStorage.multiGet(prefKeys); SERVICES_KEY.PROXIMO,
// Save those values for later use SERVICES_KEY.TUTOR_INSA,
resultArray.forEach((item: [string, string | null]) => { SERVICES_KEY.RU,
const key = item[0]; ]),
let val = item[1]; },
if (val === null) val = AsyncStorageManager.PREFERENCES[key].default; gameScores: {
this.#currentPreferences[key] = val; key: 'gameScores',
}); default: '[]',
} },
}
/**
* Saves the value associated to the given key to preferences. #currentPreferences: {[key: string]: string};
* This updates the preferences object and saves it to AsyncStorage.
* constructor() {
* @param key this.#currentPreferences = {};
* @param value }
*/
setPreference( /**
key: string, * Get this class instance or create one if none is found
// eslint-disable-next-line flowtype/no-weak-types * @returns {AsyncStorageManager}
value: number | string | boolean | {...} | Array<any>, */
) { static getInstance(): AsyncStorageManager {
if (AsyncStorageManager.PREFERENCES[key] != null) { return AsyncStorageManager.instance === null ?
let convertedValue; AsyncStorageManager.instance = new AsyncStorageManager() :
if (typeof value === 'string') convertedValue = value; AsyncStorageManager.instance;
else if (typeof value === 'boolean' || typeof value === 'number') }
convertedValue = value.toString();
else convertedValue = JSON.stringify(value); /**
this.#currentPreferences[key] = convertedValue; * Set preferences object current values from AsyncStorage.
AsyncStorage.setItem(key, convertedValue); * 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));
} }
}
/**
* 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];
}
} }

View file

@ -7,68 +7,63 @@ import i18n from 'i18n-js';
* Translations are hardcoded as toLocaleDateString does not work on current android JS engine * Translations are hardcoded as toLocaleDateString does not work on current android JS engine
*/ */
export default class DateManager { export default class DateManager {
static instance: DateManager | null = null; static instance: DateManager | null = null;
daysOfWeek = []; daysOfWeek = [];
monthsOfYear = [];
monthsOfYear = []; 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"));
constructor() { this.monthsOfYear.push(i18n.t("date.monthsOfYear.january"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday this.monthsOfYear.push(i18n.t("date.monthsOfYear.february"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.monday')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.march"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.tuesday')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.april"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.wednesday')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.may"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.thursday')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.june"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.friday')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.july"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.saturday')); 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"));
}
this.monthsOfYear.push(i18n.t('date.monthsOfYear.january')); /**
this.monthsOfYear.push(i18n.t('date.monthsOfYear.february')); * Get this class instance or create one if none is found
this.monthsOfYear.push(i18n.t('date.monthsOfYear.march')); * @returns {DateManager}
this.monthsOfYear.push(i18n.t('date.monthsOfYear.april')); */
this.monthsOfYear.push(i18n.t('date.monthsOfYear.may')); static getInstance(): DateManager {
this.monthsOfYear.push(i18n.t('date.monthsOfYear.june')); return DateManager.instance === null ?
this.monthsOfYear.push(i18n.t('date.monthsOfYear.july')); DateManager.instance = new DateManager() :
this.monthsOfYear.push(i18n.t('date.monthsOfYear.august')); DateManager.instance;
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) {
* Get this class instance or create one if none is found return date.getDay() === 6 || date.getDay() === 0;
* @returns {DateManager} }
*/
static getInstance(): DateManager {
if (DateManager.instance == null) DateManager.instance = new DateManager();
return DateManager.instance;
}
static isWeekend(date: Date): boolean { getMonthsOfYear() {
return date.getDay() === 6 || date.getDay() === 0; return this.monthsOfYear;
} }
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) {
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();
}
/**
* 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()}`;
}
} }

View file

@ -1,24 +1,22 @@
// @flow // @flow
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as RNLocalize from 'react-native-localize'; import * as RNLocalize from "react-native-localize";
import en from '../../locales/en.json'; import en from '../../locales/en';
import fr from '../../locales/fr.json'; import fr from '../../locales/fr.json';
/** /**
* Static class used to manage locales * Static class used to manage locales
*/ */
export default class LocaleManager { export default class LocaleManager {
/**
* Initialize translations using language files /**
*/ * Initialize translations using language files
static initTranslations() { */
i18n.fallbacks = true; static initTranslations() {
i18n.translations = {fr, en}; i18n.fallbacks = true;
i18n.locale = RNLocalize.findBestAvailableLanguage([ i18n.translations = {fr, en};
'en', i18n.locale = RNLocalize.findBestAvailableLanguage(["en", "fr"]).languageTag;
'fr', }
]).languageTag;
}
} }

View file

@ -1,288 +1,282 @@
// @flow // @flow
import AsyncStorageManager from "./AsyncStorageManager";
import {DarkTheme, DefaultTheme} from 'react-native-paper'; import {DarkTheme, DefaultTheme} from 'react-native-paper';
import AprilFoolsManager from "./AprilFoolsManager";
import {Appearance} from 'react-native-appearance'; import {Appearance} from 'react-native-appearance';
import AsyncStorageManager from './AsyncStorageManager';
import AprilFoolsManager from './AprilFoolsManager';
const colorScheme = Appearance.getColorScheme(); const colorScheme = Appearance.getColorScheme();
export type CustomThemeType = { export type CustomTheme = {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
primary: string, primary: string,
accent: string, accent: string,
tabIcon: string, tabIcon: string,
card: string, card: string,
dividerBackground: string, dividerBackground: string,
ripple: string, ripple: string,
textDisabled: string, textDisabled: string,
icon: string, icon: string,
subtitle: string, subtitle: string,
success: string, success: string,
warning: string, warning: string,
danger: string, danger: string,
// Calendar/Agenda // Calendar/Agenda
agendaBackgroundColor: string, agendaBackgroundColor: string,
agendaDayTextColor: string, agendaDayTextColor: string,
// PROXIWASH // PROXIWASH
proxiwashFinishedColor: string, proxiwashFinishedColor: string,
proxiwashReadyColor: string, proxiwashReadyColor: string,
proxiwashRunningColor: string, proxiwashRunningColor: string,
proxiwashRunningNotStartedColor: string, proxiwashRunningNotStartedColor: string,
proxiwashRunningBgColor: string, proxiwashRunningBgColor: string,
proxiwashBrokenColor: string, proxiwashBrokenColor: string,
proxiwashErrorColor: string, proxiwashErrorColor: string,
proxiwashUnknownColor: string, proxiwashUnknownColor: string,
// Screens // Screens
planningColor: string, planningColor: string,
proximoColor: string, proximoColor: string,
proxiwashColor: string, proxiwashColor: string,
menuColor: string, menuColor: string,
tutorinsaColor: string, tutorinsaColor: string,
// Tetris // Tetris
tetrisBackground: string, tetrisBackground: string,
tetrisBorder: string, tetrisBorder: string,
tetrisScore: string, tetrisScore: string,
tetrisI: string, tetrisI: string,
tetrisO: string, tetrisO: string,
tetrisT: string, tetrisT: string,
tetrisS: string, tetrisS: string,
tetrisZ: string, tetrisZ: string,
tetrisJ: string, tetrisJ: string,
tetrisL: string, tetrisL: string,
gameGold: string, gameGold: string,
gameSilver: string, gameSilver: string,
gameBronze: string, gameBronze: string,
// Mascot Popup // Mascot Popup
mascotMessageArrow: string, mascotMessageArrow: string,
}, },
}; }
/** /**
* Singleton class used to manage themes * Singleton class used to manage themes
*/ */
export default class ThemeManager { export default class ThemeManager {
static instance: ThemeManager | null = null;
updateThemeCallback: null | (() => void); static instance: ThemeManager | null = null;
updateThemeCallback: Function;
constructor() { constructor() {
this.updateThemeCallback = null; this.updateThemeCallback = null;
} }
/** /**
* Gets the light theme * Gets the light theme
* *
* @return {CustomThemeType} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getWhiteTheme(): CustomThemeType { static getWhiteTheme(): CustomTheme {
return { return {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
...DefaultTheme.colors, ...DefaultTheme.colors,
primary: '#be1522', primary: '#be1522',
accent: '#be1522', accent: '#be1522',
tabIcon: '#929292', tabIcon: "#929292",
card: '#fff', card: "#fff",
dividerBackground: '#e2e2e2', dividerBackground: '#e2e2e2',
ripple: 'rgba(0,0,0,0.2)', ripple: "rgba(0,0,0,0.2)",
textDisabled: '#c1c1c1', textDisabled: '#c1c1c1',
icon: '#5d5d5d', icon: '#5d5d5d',
subtitle: '#707070', subtitle: '#707070',
success: '#5cb85c', success: "#5cb85c",
warning: '#f0ad4e', warning: "#f0ad4e",
danger: '#d9534f', danger: "#d9534f",
cc: 'dst', cc: 'dst',
// Calendar/Agenda // Calendar/Agenda
agendaBackgroundColor: '#f3f3f4', agendaBackgroundColor: '#f3f3f4',
agendaDayTextColor: '#636363', agendaDayTextColor: '#636363',
// PROXIWASH // PROXIWASH
proxiwashFinishedColor: '#a5dc9d', proxiwashFinishedColor: "#a5dc9d",
proxiwashReadyColor: 'transparent', proxiwashReadyColor: "transparent",
proxiwashRunningColor: '#a0ceff', proxiwashRunningColor: "#a0ceff",
proxiwashRunningNotStartedColor: '#c9e0ff', proxiwashRunningNotStartedColor: "#c9e0ff",
proxiwashRunningBgColor: '#c7e3ff', proxiwashRunningBgColor: "#c7e3ff",
proxiwashBrokenColor: '#ffa8a2', proxiwashBrokenColor: "#ffa8a2",
proxiwashErrorColor: '#ffa8a2', proxiwashErrorColor: "#ffa8a2",
proxiwashUnknownColor: '#b6b6b6', proxiwashUnknownColor: "#b6b6b6",
// Screens // Screens
planningColor: '#d9b10a', planningColor: '#d9b10a',
proximoColor: '#ec5904', proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee', proxiwashColor: '#1fa5ee',
menuColor: '#e91314', menuColor: '#e91314',
tutorinsaColor: '#f93943', tutorinsaColor: '#f93943',
// Tetris // Tetris
tetrisBackground: '#f0f0f0', tetrisBackground: '#f0f0f0',
tetrisScore: '#e2bd33', tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6', tetrisI: '#3cd9e6',
tetrisO: '#ffdd00', tetrisO: '#ffdd00',
tetrisT: '#a716e5', tetrisT: '#a716e5',
tetrisS: '#09c528', tetrisS: '#09c528',
tetrisZ: '#ff0009', tetrisZ: '#ff0009',
tetrisJ: '#2a67e3', tetrisJ: '#2a67e3',
tetrisL: '#da742d', tetrisL: '#da742d',
gameGold: '#ffd610', gameGold: "#ffd610",
gameSilver: '#7b7b7b', gameSilver: "#7b7b7b",
gameBronze: '#a15218', gameBronze: "#a15218",
// Mascot Popup // Mascot Popup
mascotMessageArrow: '#dedede', mascotMessageArrow: "#dedede",
}, },
}; };
} }
/** /**
* Gets the dark theme * Gets the dark theme
* *
* @return {CustomThemeType} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getDarkTheme(): CustomThemeType { static getDarkTheme(): CustomTheme {
return { return {
...DarkTheme, ...DarkTheme,
colors: { colors: {
...DarkTheme.colors, ...DarkTheme.colors,
primary: '#be1522', primary: '#be1522',
accent: '#be1522', accent: '#be1522',
tabBackground: '#181818', tabBackground: "#181818",
tabIcon: '#6d6d6d', tabIcon: "#6d6d6d",
card: 'rgb(18,18,18)', card: "rgb(18,18,18)",
dividerBackground: '#222222', dividerBackground: '#222222',
ripple: 'rgba(255,255,255,0.2)', ripple: "rgba(255,255,255,0.2)",
textDisabled: '#5b5b5b', textDisabled: '#5b5b5b',
icon: '#b3b3b3', icon: '#b3b3b3',
subtitle: '#aaaaaa', subtitle: '#aaaaaa',
success: '#5cb85c', success: "#5cb85c",
warning: '#f0ad4e', warning: "#f0ad4e",
danger: '#d9534f', danger: "#d9534f",
// Calendar/Agenda // Calendar/Agenda
agendaBackgroundColor: '#171717', agendaBackgroundColor: '#171717',
agendaDayTextColor: '#6d6d6d', agendaDayTextColor: '#6d6d6d',
// PROXIWASH // PROXIWASH
proxiwashFinishedColor: '#31682c', proxiwashFinishedColor: "#31682c",
proxiwashReadyColor: 'transparent', proxiwashReadyColor: "transparent",
proxiwashRunningColor: '#213c79', proxiwashRunningColor: "#213c79",
proxiwashRunningNotStartedColor: '#1e263e', proxiwashRunningNotStartedColor: "#1e263e",
proxiwashRunningBgColor: '#1a2033', proxiwashRunningBgColor: "#1a2033",
proxiwashBrokenColor: '#7e2e2f', proxiwashBrokenColor: "#7e2e2f",
proxiwashErrorColor: '#7e2e2f', proxiwashErrorColor: "#7e2e2f",
proxiwashUnknownColor: '#535353', proxiwashUnknownColor: "#535353",
// Screens // Screens
planningColor: '#d99e09', planningColor: '#d99e09',
proximoColor: '#ec5904', proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee', proxiwashColor: '#1fa5ee',
menuColor: '#b81213', menuColor: '#b81213',
tutorinsaColor: '#f93943', tutorinsaColor: '#f93943',
// Tetris // Tetris
tetrisBackground: '#181818', tetrisBackground: '#181818',
tetrisScore: '#e2d707', tetrisScore: '#e2d707',
tetrisI: '#30b3be', tetrisI: '#30b3be',
tetrisO: '#c1a700', tetrisO: '#c1a700',
tetrisT: '#9114c7', tetrisT: '#9114c7',
tetrisS: '#08a121', tetrisS: '#08a121',
tetrisZ: '#b50008', tetrisZ: '#b50008',
tetrisJ: '#0f37b9', tetrisJ: '#0f37b9',
tetrisL: '#b96226', tetrisL: '#b96226',
gameGold: '#ffd610', gameGold: "#ffd610",
gameSilver: '#7b7b7b', gameSilver: "#7b7b7b",
gameBronze: '#a15218', gameBronze: "#a15218",
// Mascot Popup // Mascot Popup
mascotMessageArrow: '#323232', mascotMessageArrow: "#323232",
}, },
}; };
} }
/** /**
* Get this class instance or create one if none is found * Get this class instance or create one if none is found
* *
* @returns {ThemeManager} * @returns {ThemeManager}
*/ */
static getInstance(): ThemeManager { static getInstance(): ThemeManager {
if (ThemeManager.instance == null) return ThemeManager.instance === null ?
ThemeManager.instance = new ThemeManager(); ThemeManager.instance = new ThemeManager() :
return ThemeManager.instance; ThemeManager.instance;
} }
/** /**
* Gets night mode status. * Gets night mode status.
* If Follow System Preferences is enabled, will first use system theme. * If Follow System Preferences is enabled, will first use system theme.
* If disabled or not available, will use value stored din preferences * If disabled or not available, will use value stored din preferences
* *
* @returns {boolean} Night mode state * @returns {boolean} Night mode state
*/ */
static getNightMode(): boolean { static getNightMode(): boolean {
return ( return (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightMode.key) &&
(AsyncStorageManager.getBool( (!AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
AsyncStorageManager.PREFERENCES.nightMode.key, || colorScheme === 'no-preference')) ||
) && (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
(!AsyncStorageManager.getBool( && colorScheme === 'dark');
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 * Get the current theme based on night mode and events
* *
* @returns {CustomThemeType} The current theme * @returns {CustomTheme} The current theme
*/ */
static getCurrentTheme(): CustomThemeType { static getCurrentTheme(): CustomTheme {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme()); return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
return ThemeManager.getBaseTheme(); else
} return ThemeManager.getBaseTheme()
}
/** /**
* Get the theme based on night mode * Get the theme based on night mode
* *
* @return {CustomThemeType} The theme * @return {CustomTheme} The theme
*/ */
static getBaseTheme(): CustomThemeType { static getBaseTheme(): CustomTheme {
if (ThemeManager.getNightMode()) return ThemeManager.getDarkTheme(); if (ThemeManager.getNightMode())
return ThemeManager.getWhiteTheme(); return ThemeManager.getDarkTheme();
} else
return ThemeManager.getWhiteTheme();
}
/** /**
* Sets the function to be called when the theme is changed (allows for general reload of the app) * 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 * @param callback Function to call after theme change
*/ */
setUpdateThemeCallback(callback: () => void) { setUpdateThemeCallback(callback: () => void) {
this.updateThemeCallback = callback; this.updateThemeCallback = callback;
} }
/** /**
* Set night mode and save it to preferences * Set night mode and save it to preferences
* *
* @param isNightMode True to enable night mode, false to disable * @param isNightMode True to enable night mode, false to disable
*/ */
setNightMode(isNightMode: boolean) { setNightMode(isNightMode: boolean) {
AsyncStorageManager.set( AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.nightMode.key, isNightMode);
AsyncStorageManager.PREFERENCES.nightMode.key, if (this.updateThemeCallback != null)
isNightMode, this.updateThemeCallback();
); }
if (this.updateThemeCallback != null) this.updateThemeCallback();
} };
}

View file

@ -1,231 +1,204 @@
// @flow // @flow
import * as React from 'react'; 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 SettingsScreen from '../screens/Other/Settings/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
import DebugScreen from '../screens/About/DebugScreen'; import DebugScreen from '../screens/About/DebugScreen';
import TabNavigator from './TabNavigator'; import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
import GameMainScreen from '../screens/Game/screens/GameMainScreen'; import i18n from "i18n-js";
import VoteScreen from '../screens/Amicale/VoteScreen'; import TabNavigator from "./TabNavigator";
import LoginScreen from '../screens/Amicale/LoginScreen'; import GameMainScreen from "../screens/Game/screens/GameMainScreen";
import SelfMenuScreen from '../screens/Services/SelfMenuScreen'; import VoteScreen from "../screens/Amicale/VoteScreen";
import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen'; import LoginScreen from "../screens/Amicale/LoginScreen";
import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen'; import {Platform} from "react-native";
import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen'; import SelfMenuScreen from "../screens/Services/SelfMenuScreen";
import ProfileScreen from '../screens/Amicale/ProfileScreen'; import ProximoMainScreen from "../screens/Services/Proximo/ProximoMainScreen";
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen'; import ProximoListScreen from "../screens/Services/Proximo/ProximoListScreen";
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen'; import ProximoAboutScreen from "../screens/Services/Proximo/ProximoAboutScreen";
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ProfileScreen from "../screens/Amicale/ProfileScreen";
import { import ClubListScreen from "../screens/Amicale/Clubs/ClubListScreen";
createScreenCollapsibleStack, import ClubAboutScreen from "../screens/Amicale/Clubs/ClubAboutScreen";
getWebsiteStack, import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
} from '../utils/CollapsibleUtils'; import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
import BugReportScreen from '../screens/Other/FeedbackScreen'; import BugReportScreen from "../screens/Other/FeedbackScreen";
import WebsiteScreen from '../screens/Services/WebsiteScreen'; import WebsiteScreen from "../screens/Services/WebsiteScreen";
import EquipmentScreen from '../screens/Amicale/Equipment/EquipmentListScreen'; import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen'; import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen'; import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import GameStartScreen from "../screens/Game/screens/GameStartScreen";
const modalTransition = const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalSlideFromBottomIOS;
const defaultScreenOptions = { const defaultScreenOptions = {
gestureEnabled: true, gestureEnabled: true,
cardOverlayEnabled: true, cardOverlayEnabled: true,
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}; };
const MainStack = createStackNavigator(); const MainStack = createStackNavigator();
function MainStackComponent(props: { function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
createTabNavigator: () => React.Node, return (
}): React.Node { <MainStack.Navigator
const {createTabNavigator} = props; initialRouteName={'main'}
return ( headerMode={'screen'}
<MainStack.Navigator screenOptions={defaultScreenOptions}
initialRouteName="main" >
headerMode="screen" <MainStack.Screen
screenOptions={defaultScreenOptions}> name="main"
<MainStack.Screen component={props.createTabNavigator}
name="main" options={{
component={createTabNavigator} headerShown: false,
options={{ title: i18n.t('screens.home.title'),
headerShown: false, }}
title: i18n.t('screens.home.title'), />
}} {createScreenCollapsibleStack(
/> "settings",
{createScreenCollapsibleStack( MainStack,
'settings', SettingsScreen,
MainStack, i18n.t('screens.settings.title'))}
SettingsScreen, {createScreenCollapsibleStack(
i18n.t('screens.settings.title'), "dashboard-edit",
)} MainStack,
{createScreenCollapsibleStack( DashboardEditScreen,
'dashboard-edit', i18n.t('screens.settings.dashboardEdit.title'))}
MainStack, {createScreenCollapsibleStack(
DashboardEditScreen, "about",
i18n.t('screens.settings.dashboardEdit.title'), MainStack,
)} AboutScreen,
{createScreenCollapsibleStack( i18n.t('screens.about.title'))}
'about', {createScreenCollapsibleStack(
MainStack, "dependencies",
AboutScreen, MainStack,
i18n.t('screens.about.title'), AboutDependenciesScreen,
)} i18n.t('screens.about.libs'))}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'dependencies', "debug",
MainStack, MainStack,
AboutDependenciesScreen, DebugScreen,
i18n.t('screens.about.libs'), i18n.t('screens.about.debug'))}
)}
{createScreenCollapsibleStack(
'debug',
MainStack,
DebugScreen,
i18n.t('screens.about.debug'),
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'game-start', "game-start",
MainStack, MainStack,
GameStartScreen, GameStartScreen,
i18n.t('screens.game.title'), i18n.t('screens.game.title'))}
)} <MainStack.Screen
<MainStack.Screen name="game-main"
name="game-main" component={GameMainScreen}
component={GameMainScreen} options={{
options={{ title: i18n.t("screens.game.title"),
title: i18n.t('screens.game.title'), }}
}} />
/> {createScreenCollapsibleStack(
{createScreenCollapsibleStack( "login",
'login', MainStack,
MainStack, LoginScreen,
LoginScreen, i18n.t('screens.login.title'),
i18n.t('screens.login.title'), true,
true, {headerTintColor: "#fff"},
{headerTintColor: '#fff'}, 'transparent')}
'transparent', {getWebsiteStack("website", MainStack, WebsiteScreen, "")}
)}
{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( {createScreenCollapsibleStack(
'profile', "self-menu",
MainStack, MainStack,
ProfileScreen, SelfMenuScreen,
i18n.t('screens.profile.title'), i18n.t('screens.menu.title'))}
)} {createScreenCollapsibleStack(
{createScreenCollapsibleStack( "proximo",
'club-list', MainStack,
MainStack, ProximoMainScreen,
ClubListScreen, i18n.t('screens.proximo.title'))}
i18n.t('screens.clubs.title'), {createScreenCollapsibleStack(
)} "proximo-list",
{createScreenCollapsibleStack( MainStack,
'club-information', ProximoListScreen,
MainStack, i18n.t('screens.proximo.articleList'),
ClubDisplayScreen, )}
i18n.t('screens.clubs.details'), {createScreenCollapsibleStack(
true, "proximo-about",
{...modalTransition}, MainStack,
)} ProximoAboutScreen,
{createScreenCollapsibleStack( i18n.t('screens.proximo.title'),
'club-about', true,
MainStack, {...modalTransition},
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 = { {createScreenCollapsibleStack(
defaultHomeRoute: string | null, "profile",
// eslint-disable-next-line flowtype/no-weak-types MainStack,
defaultHomeData: {[key: string]: string}, ProfileScreen,
}; i18n.t('screens.profile.title'))}
{createScreenCollapsibleStack(
export default class MainNavigator extends React.Component<PropsType> { "club-list",
createTabNavigator: () => React.Node; MainStack,
ClubListScreen,
constructor(props: PropsType) { i18n.t('screens.clubs.title'))}
super(props); {createScreenCollapsibleStack(
this.createTabNavigator = (): React.Node => ( "club-information",
<TabNavigator MainStack,
defaultHomeRoute={props.defaultHomeRoute} ClubDisplayScreen,
defaultHomeData={props.defaultHomeData} 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>
); );
} }
render(): React.Node { type Props = {
return <MainStackComponent createTabNavigator={this.createTabNavigator} />; 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}/>
);
}
} }

View file

@ -1,292 +1,271 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack'; 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 HomeScreen from '../screens/Home/HomeScreen';
import PlanningScreen from '../screens/Planning/PlanningScreen'; import PlanningScreen from '../screens/Planning/PlanningScreen';
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen'; import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen'; import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen'; import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
import PlanexScreen from '../screens/Planex/PlanexScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen';
import AsyncStorageManager from '../managers/AsyncStorageManager'; import AsyncStorageManager from "../managers/AsyncStorageManager";
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import {Title, useTheme} from 'react-native-paper';
import ScannerScreen from '../screens/Home/ScannerScreen'; import {Platform} from 'react-native';
import FeedItemScreen from '../screens/Home/FeedItemScreen'; import i18n from "i18n-js";
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen'; import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import CustomTabBar from '../components/Tabbar/CustomTabBar'; import ScannerScreen from "../screens/Home/ScannerScreen";
import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import FeedItemScreen from "../screens/Home/FeedItemScreen";
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import {createCollapsibleStack} from "react-navigation-collapsible";
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen";
import { import CustomTabBar from "../components/Tabbar/CustomTabBar";
createScreenCollapsibleStack, import WebsitesHomeScreen from "../screens/Services/ServicesScreen";
getWebsiteStack, import ServicesSectionScreen from "../screens/Services/ServicesSectionScreen";
} from '../utils/CollapsibleUtils'; import AmicaleContactScreen from "../screens/Amicale/AmicaleContactScreen";
import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot'; 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;
const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalSlideFromBottomIOS;
const defaultScreenOptions = { const defaultScreenOptions = {
gestureEnabled: true, gestureEnabled: true,
cardOverlayEnabled: true, cardOverlayEnabled: true,
...modalTransition, ...modalTransition,
}; };
const ServicesStack = createStackNavigator(); const ServicesStack = createStackNavigator();
function ServicesStackComponent(): React.Node { function ServicesStackComponent() {
return ( return (
<ServicesStack.Navigator <ServicesStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
{createScreenCollapsibleStack( >
'index', {createScreenCollapsibleStack(
ServicesStack, "index",
WebsitesHomeScreen, ServicesStack,
i18n.t('screens.services.title'), WebsitesHomeScreen,
)} i18n.t('screens.services.title'))}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'services-section', "services-section",
ServicesStack, ServicesStack,
ServicesSectionScreen, ServicesSectionScreen,
'SECTION', "SECTION")}
)} {createScreenCollapsibleStack(
{createScreenCollapsibleStack( "amicale-contact",
'amicale-contact', ServicesStack,
ServicesStack, AmicaleContactScreen,
AmicaleContactScreen, i18n.t('screens.amicaleAbout.title'))}
i18n.t('screens.amicaleAbout.title'), </ServicesStack.Navigator>
)} );
</ServicesStack.Navigator>
);
} }
const ProxiwashStack = createStackNavigator(); const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent(): React.Node { function ProxiwashStackComponent() {
return ( return (
<ProxiwashStack.Navigator <ProxiwashStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
{createScreenCollapsibleStack( >
'index', {createScreenCollapsibleStack(
ProxiwashStack, "index",
ProxiwashScreen, ProxiwashStack,
i18n.t('screens.proxiwash.title'), ProxiwashScreen,
)} i18n.t('screens.proxiwash.title'))}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'proxiwash-about', "proxiwash-about",
ProxiwashStack, ProxiwashStack,
ProxiwashAboutScreen, ProxiwashAboutScreen,
i18n.t('screens.proxiwash.title'), i18n.t('screens.proxiwash.title'))}
)} </ProxiwashStack.Navigator>
</ProxiwashStack.Navigator> );
);
} }
const PlanningStack = createStackNavigator(); const PlanningStack = createStackNavigator();
function PlanningStackComponent(): React.Node { function PlanningStackComponent() {
return ( return (
<PlanningStack.Navigator <PlanningStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
<PlanningStack.Screen >
name="index" <PlanningStack.Screen
component={PlanningScreen} name="index"
options={{title: i18n.t('screens.planning.title')}} component={PlanningScreen}
/> options={{title: i18n.t('screens.planning.title'),}}
{createScreenCollapsibleStack( />
'planning-information', {createScreenCollapsibleStack(
PlanningStack, "planning-information",
PlanningDisplayScreen, PlanningStack,
i18n.t('screens.planning.eventDetails'), PlanningDisplayScreen,
)} i18n.t('screens.planning.eventDetails'))}
</PlanningStack.Navigator> </PlanningStack.Navigator>
); );
} }
const HomeStack = createStackNavigator(); const HomeStack = createStackNavigator();
function HomeStackComponent( function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) {
initialRoute: string | null, let params = undefined;
defaultData: {[key: string]: string}, if (initialRoute != null)
): React.Node { params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
let params; const {colors} = useTheme();
if (initialRoute != null) return (
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true}; <HomeStack.Navigator
const {colors} = useTheme(); initialRouteName={"index"}
return ( headerMode={"screen"}
<HomeStack.Navigator screenOptions={defaultScreenOptions}
initialRouteName="index" >
headerMode="screen" {createCollapsibleStack(
screenOptions={defaultScreenOptions}> <HomeStack.Screen
{createCollapsibleStack( name="index"
<HomeStack.Screen component={HomeScreen}
name="index" options={{
component={HomeScreen} title: i18n.t('screens.home.title'),
options={{ headerStyle: {
title: i18n.t('screens.home.title'), backgroundColor: colors.surface,
headerStyle: { },
backgroundColor: colors.surface, headerTitle: () =>
}, <View style={{flexDirection: "row"}}>
headerTitle: (): React.Node => ( <Mascot
<View style={{flexDirection: 'row'}}> style={{
<Mascot width: 50
style={{ }}
width: 50, emotion={MASCOT_STYLE.RANDOM}
}} animated={true}
emotion={MASCOT_STYLE.RANDOM} entryAnimation={{
animated animation: "bounceIn",
entryAnimation={{ duration: 1000
animation: 'bounceIn', }}
duration: 1000, loopAnimation={{
}} animation: "pulse",
loopAnimation={{ duration: 2000,
animation: 'pulse', iterationCount: "infinite"
duration: 2000, }}
iterationCount: 'infinite', />
}} <Title style={{
/> marginLeft: 10,
<Title marginTop: "auto",
style={{ marginBottom: "auto",
marginLeft: 10, }}>{i18n.t('screens.home.title')}</Title>
marginTop: 'auto', </View>
marginBottom: 'auto', }}
}}> initialParams={params}
{i18n.t('screens.home.title')} />,
</Title> {
</View> collapsedColor: colors.surface,
), useNativeDriver: true,
}} }
initialParams={params} )}
/>, <HomeStack.Screen
{ name="scanner"
collapsedColor: colors.surface, component={ScannerScreen}
useNativeDriver: true, options={{title: i18n.t('screens.scanner.title'),}}
}, />
)}
<HomeStack.Screen
name="scanner"
component={ScannerScreen}
options={{title: i18n.t('screens.scanner.title')}}
/>
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'club-information', "club-information",
HomeStack, HomeStack,
ClubDisplayScreen, ClubDisplayScreen,
i18n.t('screens.clubs.details'), i18n.t('screens.clubs.details'))}
)} {createScreenCollapsibleStack(
{createScreenCollapsibleStack( "feed-information",
'feed-information', HomeStack,
HomeStack, FeedItemScreen,
FeedItemScreen, i18n.t('screens.home.feed'))}
i18n.t('screens.home.feed'), {createScreenCollapsibleStack(
)} "planning-information",
{createScreenCollapsibleStack( HomeStack,
'planning-information', PlanningDisplayScreen,
HomeStack, i18n.t('screens.planning.eventDetails'))}
PlanningDisplayScreen, </HomeStack.Navigator>
i18n.t('screens.planning.eventDetails'), );
)}
</HomeStack.Navigator>
);
} }
const PlanexStack = createStackNavigator(); const PlanexStack = createStackNavigator();
function PlanexStackComponent(): React.Node { function PlanexStackComponent() {
return ( return (
<PlanexStack.Navigator <PlanexStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
{getWebsiteStack( >
'index', {getWebsiteStack(
PlanexStack, "index",
PlanexScreen, PlanexStack,
i18n.t('screens.planex.title'), PlanexScreen,
)} i18n.t("screens.planex.title"))}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'group-select', "group-select",
PlanexStack, PlanexStack,
GroupSelectionScreen, GroupSelectionScreen,
'', "")}
)} </PlanexStack.Navigator>
</PlanexStack.Navigator> );
);
} }
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
type PropsType = { type Props = {
defaultHomeRoute: string | null, defaultHomeRoute: string | null,
defaultHomeData: {[key: string]: string}, defaultHomeData: { [key: string]: any }
}; }
export default class TabNavigator extends React.Component<PropsType> { export default class TabNavigator extends React.Component<Props> {
createHomeStackComponent: () => React.Node;
createHomeStackComponent: () => HomeStackComponent;
defaultRoute: string; defaultRoute: string;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
if (props.defaultHomeRoute != null) this.defaultRoute = 'home'; if (props.defaultHomeRoute != null)
else this.defaultRoute = 'home';
this.defaultRoute = AsyncStorageManager.getString( else
AsyncStorageManager.PREFERENCES.defaultStartScreen.key, this.defaultRoute = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key).toLowerCase();
).toLowerCase(); this.createHomeStackComponent = () => HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
this.createHomeStackComponent = (): React.Node => }
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
} render() {
return (
render(): React.Node { <Tab.Navigator
return ( initialRouteName={this.defaultRoute}
<Tab.Navigator tabBar={props => <CustomTabBar {...props} />}
initialRouteName={this.defaultRoute} >
// eslint-disable-next-line react/jsx-props-no-spreading <Tab.Screen
tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}> name="services"
<Tab.Screen option
name="services" component={ServicesStackComponent}
option options={{title: i18n.t('screens.services.title')}}
component={ServicesStackComponent} />
options={{title: i18n.t('screens.services.title')}} <Tab.Screen
/> name="proxiwash"
<Tab.Screen component={ProxiwashStackComponent}
name="proxiwash" options={{title: i18n.t('screens.proxiwash.title')}}
component={ProxiwashStackComponent} />
options={{title: i18n.t('screens.proxiwash.title')}} <Tab.Screen
/> name="home"
<Tab.Screen component={this.createHomeStackComponent}
name="home" options={{title: i18n.t('screens.home.title')}}
component={this.createHomeStackComponent} />
options={{title: i18n.t('screens.home.title')}} <Tab.Screen
/> name="planning"
<Tab.Screen component={PlanningStackComponent}
name="planning" options={{title: i18n.t('screens.planning.title')}}
component={PlanningStackComponent} />
options={{title: i18n.t('screens.planning.title')}}
/> <Tab.Screen
name="planex"
<Tab.Screen component={PlanexStackComponent}
name="planex" options={{title: i18n.t("screens.planex.title")}}
component={PlanexStackComponent} />
options={{title: i18n.t('screens.planex.title')}} </Tab.Navigator>
/> );
</Tab.Navigator> }
);
}
} }

View file

@ -1,31 +1,36 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import packageJson from '../../../package';
import {List} from 'react-native-paper'; import {List} from 'react-native-paper';
import {View} from 'react-native-animatable'; import {StackNavigationProp} from "@react-navigation/stack";
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import packageJson from '../../../package.json'; import {View} from "react-native-animatable";
type ListItemType = { type listItem = {
name: string, name: string,
version: string, version: string
}; };
/** /**
* Generates the dependencies list from the raw json * Generates the dependencies list from the raw json
* *
* @param object The raw json * @param object The raw json
* @return {Array<ListItemType>} * @return {Array<listItem>}
*/ */
function generateListFromObject(object: { function generateListFromObject(object: { [key: string]: string }): Array<listItem> {
[key: string]: string, let list = [];
}): Array<ListItemType> { let keys = Object.keys(object);
const list = []; let values = Object.values(object);
const keys = Object.keys(object); for (let i = 0; i < keys.length; i++) {
keys.forEach((key: string) => { list.push({name: keys[i], version: values[i]});
list.push({name: key, version: object[key]}); }
}); //$FlowFixMe
return list; return list;
}
type Props = {
navigation: StackNavigationProp,
} }
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -33,45 +38,38 @@ const LIST_ITEM_HEIGHT = 64;
/** /**
* Class defining a screen showing the list of libraries used by the app, taken from package.json * Class defining a screen showing the list of libraries used by the app, taken from package.json
*/ */
export default class AboutDependenciesScreen extends React.Component<null> { export default class AboutDependenciesScreen extends React.Component<Props> {
data: Array<ListItemType>;
constructor() { data: Array<listItem>;
super();
this.data = generateListFromObject(packageJson.dependencies);
}
keyExtractor = (item: ListItemType): string => item.name; constructor() {
super();
this.data = generateListFromObject(packageJson.dependencies);
}
getRenderItem = ({item}: {item: ListItemType}): React.Node => ( keyExtractor = (item: listItem) => item.name;
<List.Item
title={item.name}
description={item.version.replace('^', '').replace('~', '')}
style={{height: LIST_ITEM_HEIGHT}}
/>
);
getItemLayout = ( renderItem = ({item}: { item: listItem }) =>
data: ListItemType, <List.Item
index: number, title={item.name}
): {length: number, offset: number, index: number} => ({ description={item.version.replace('^', '').replace('~', '')}
length: LIST_ITEM_HEIGHT, style={{height: LIST_ITEM_HEIGHT}}
offset: LIST_ITEM_HEIGHT * index, />;
index,
});
render(): React.Node { itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
return (
<View> render() {
<CollapsibleFlatList return (
data={this.data} <View>
keyExtractor={this.keyExtractor} <CollapsibleFlatList
renderItem={this.getRenderItem} data={this.data}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration keyExtractor={this.keyExtractor}
removeClippedSubviews renderItem={this.renderItem}
getItemLayout={this.getItemLayout} // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
/> removeClippedSubviews={true}
</View> getItemLayout={this.itemLayout}
); />
} </View>
);
}
} }

View file

@ -1,388 +1,351 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, Linking, Platform} from 'react-native'; import {FlatList, Linking, Platform, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper'; import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import packageJson from "../../../package.json";
import packageJson from '../../../package.json'; import {StackNavigationProp} from "@react-navigation/stack";
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import APP_LOGO from '../../../assets/android.icon.png';
type ListItemType = { type ListItem = {
onPressCallback: () => void, onPressCallback: () => void,
icon: string, icon: string,
text: string, text: string,
showChevron: boolean, showChevron: boolean
}; };
const links = { const links = {
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148', appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
playstore: playstore: 'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
'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',
git: changelog: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md', license: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
changelog: authorMail: "mailto:vergnet@etud.insa-toulouse.fr?" +
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md', "subject=" +
license: "Application Amicale INSA Toulouse" +
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE', "&body=" +
authorMail: "Coucou !\n\n",
'mailto:vergnet@etud.insa-toulouse.fr?' + authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/',
'subject=' + yohanMail: "mailto:ysimard@etud.insa-toulouse.fr?" +
'Application Amicale INSA Toulouse' + "subject=" +
'&body=' + "Application Amicale INSA Toulouse" +
'Coucou !\n\n', "&body=" +
authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/', "Coucou !\n\n",
yohanMail: yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard',
'mailto:ysimard@etud.insa-toulouse.fr?' + react: 'https://facebook.github.io/react-native/',
'subject=' + meme: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
'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 PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
}; };
/** /**
* Opens a link in the device's browser * Opens a link in the device's browser
* @param link The link to open * @param link The link to open
*/ */
function openWebLink(link: string) { function openWebLink(link) {
Linking.openURL(link); Linking.openURL(link).catch((err) => console.error('Error opening link', err));
} }
/** /**
* Class defining an about screen. This screen shows the user information about the app and it's author. * Class defining an about screen. This screen shows the user information about the app and it's author.
*/ */
class AboutScreen extends React.Component<PropsType> { class AboutScreen extends React.Component<Props> {
/**
* 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 author card * Data to be displayed in the app card
*/ */
authorData = [ appData = [
{ {
onPressCallback: () => { onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore),
openWebLink(links.meme); icon: Platform.OS === "ios" ? 'apple' : 'google-play',
}, text: Platform.OS === "ios" ? i18n.t('screens.about.appstore') : i18n.t('screens.about.playstore'),
icon: 'account-circle', showChevron: true
text: 'Arnaud VERGNET', },
showChevron: false, {
}, onPressCallback: () => this.props.navigation.navigate("feedback"),
{ icon: 'bug',
onPressCallback: () => { text: i18n.t("screens.feedback.homeButtonTitle"),
openWebLink(links.authorMail); showChevron: true
}, },
icon: 'email', {
text: i18n.t('screens.about.authorMail'), onPressCallback: () => openWebLink(links.git),
showChevron: true, icon: 'git',
}, text: 'Git',
{ showChevron: true
onPressCallback: () => { },
openWebLink(links.authorLinkedin); {
}, onPressCallback: () => openWebLink(links.changelog),
icon: 'linkedin', icon: 'refresh',
text: 'Linkedin', text: i18n.t('screens.about.changelog'),
showChevron: true, 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 additional developer card * Gets the app icon
*/ *
additionalDevData = [ * @param props
{ * @return {*}
onPressCallback: () => {}, */
icon: 'account', getAppIcon(props) {
text: 'Yohan SIMARD', return (
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 <Avatar.Image
size={size} {...props}
source={APP_LOGO} source={require('../../../assets/android.icon.png')}
style={{backgroundColor: 'transparent'}} 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}
/>
);
};
/** /**
* Gets a card, depending on the given item's id * Extracts a key from the given item
* *
* @param item The item to show * @param item The item to extract the key from
* @return {*} * @return {string} The extracted key
*/ */
getMainCard = ({item}: {item: {id: string}}): React.Node => { keyExtractor(item: ListItem): string {
switch (item.id) { return item.icon;
case 'app':
return this.getAppCard();
case 'team':
return this.getTeamCard();
case 'techno':
return this.getTechnoCard();
default:
return null;
} }
};
/** /**
* Extracts a key from the given item * Gets the app card showing information and links about the app.
* *
* @param item The item to extract the key from * @return {*}
* @return {string} The extracted key */
*/ getAppCard() {
keyExtractor = (item: ListItemType): string => item.icon; 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>
);
}
render(): React.Node { /**
return ( * Gets the team card showing information and links about the team
<CollapsibleFlatList *
style={{padding: 5}} * @return {*}
data={this.dataOrder} */
renderItem={this.getMainCard} 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}
/>
);
}
} }
export default withTheme(AboutScreen); export default withTheme(AboutScreen);

View file

@ -1,211 +1,183 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import { import AsyncStorageManager from "../../managers/AsyncStorageManager";
Button, import CustomModal from "../../components/Overrides/CustomModal";
List, import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';
Subheading, import {StackNavigationProp} from "@react-navigation/stack";
TextInput, import {Modalize} from "react-native-modalize";
Title, import type {CustomTheme} from "../../managers/ThemeManager";
withTheme, import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
} 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 PreferenceItemType = { type PreferenceItem = {
key: string, key: string,
default: string, default: string,
current: string, current: string,
}
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme
}; };
type PropsType = { type State = {
theme: CustomThemeType, modalCurrentDisplayItem: PreferenceItem,
}; currentPreferences: Array<PreferenceItem>,
}
type StateType = {
modalCurrentDisplayItem: PreferenceItemType,
currentPreferences: Array<PreferenceItemType>,
};
/** /**
* Class defining the Debug screen. * Class defining the Debug screen.
* This screen allows the user to get and modify information on the app/device. * This screen allows the user to get and modify information on the app/device.
*/ */
class DebugScreen extends React.Component<PropsType, StateType> { class DebugScreen extends React.Component<Props, State> {
modalRef: Modalize;
modalInputValue: string; modalRef: Modalize;
modalInputValue: string;
/** /**
* Copies user preferences to state for easier manipulation * Copies user preferences to state for easier manipulation
* *
* @param props * @param props
*/ */
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.modalInputValue = ''; this.modalInputValue = "";
const currentPreferences: Array<PreferenceItemType> = []; let currentPreferences : Array<PreferenceItem> = [];
// eslint-disable-next-line flowtype/no-weak-types Object.values(AsyncStorageManager.PREFERENCES).map((object: any) => {
Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => { let newObject: PreferenceItem = {...object};
const newObject: PreferenceItemType = {...object}; newObject.current = AsyncStorageManager.getString(newObject.key);
newObject.current = AsyncStorageManager.getString(newObject.key); currentPreferences.push(newObject);
currentPreferences.push(newObject); });
}); this.state = {
this.state = { modalCurrentDisplayItem: {},
modalCurrentDisplayItem: {}, currentPreferences: currentPreferences
currentPreferences, };
};
}
/**
* 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 * Shows the edit modal
* *
* @param key The pref key * @param item
* @param value The pref value */
*/ showEditModal(item: PreferenceItem) {
saveNewPrefs(key: string, value: string) { this.setState({
this.setState((prevState: StateType): { modalCurrentDisplayItem: item
currentPreferences: Array<PreferenceItemType>, });
} => { if (this.modalRef) {
const currentPreferences = [...prevState.currentPreferences]; this.modalRef.open();
currentPreferences[this.findIndexOfKey(key)].current = value; }
return {currentPreferences}; }
});
AsyncStorageManager.set(key, value);
this.modalRef.close();
}
render(): React.Node { /**
const {state} = this; * Gets the edit modal content
return ( *
<View> * @return {*}
<CustomModal onRef={this.onModalRef}> */
{this.getModalContent()} getModalContent() {
</CustomModal> return (
{/* $FlowFixMe */} <View style={{
<CollapsibleFlatList flex: 1,
data={state.currentPreferences} padding: 20
extraData={state.currentPreferences} }}>
renderItem={this.getRenderItem} <Title>{this.state.modalCurrentDisplayItem.key}</Title>
/> <Subheading>Default: {this.state.modalCurrentDisplayItem.default}</Subheading>
</View> <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)}
/>
);
};
render() {
return (
<View>
<CustomModal onRef={this.onModalRef}>
{this.getModalContent()}
</CustomModal>
{/*$FlowFixMe*/}
<CollapsibleFlatList
data={this.state.currentPreferences}
extraData={this.state.currentPreferences}
renderItem={this.renderItem}
/>
</View>
);
}
} }
export default withTheme(DebugScreen); export default withTheme(DebugScreen);

View file

@ -4,157 +4,137 @@ import * as React from 'react';
import {FlatList, Image, Linking, View} from 'react-native'; import {FlatList, Image, Linking, View} from 'react-native';
import {Card, List, Text, withTheme} from 'react-native-paper'; import {Card, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons'; import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import AMICALE_LOGO from '../../../assets/amicale.png';
type DatasetItemType = { type Props = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
}; };
type DatasetItem = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
}
/** /**
* Class defining a planning event information page. * Class defining a planning event information page.
*/ */
class AmicaleContactScreen extends React.Component<null> { class AmicaleContactScreen extends React.Component<Props> {
// Dataset containing information about contacts
CONTACT_DATASET: Array<DatasetItemType>;
constructor() { // Dataset containing information about contacts
super(); CONTACT_DATASET: Array<DatasetItem>;
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; 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"
},
];
}
getChevronIcon = ({ keyExtractor = (item: DatasetItem) => item.email;
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
);
getRenderItem = ({item}: {item: DatasetItemType}): React.Node => { getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
const onPress = () => {
Linking.openURL(`mailto:${item.email}`); 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}
/>
}; };
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 = (): React.Node => { getScreen = () => {
return ( return (
<View> <View>
<View <View style={{
style={{ width: '100%',
width: '100%', height: 100,
height: 100, marginTop: 20,
marginTop: 20, marginBottom: 20,
marginBottom: 20, justifyContent: 'center',
justifyContent: 'center', alignItems: 'center'
alignItems: 'center', }}>
}}> <Image
<Image source={require('../../../assets/amicale.png')}
source={AMICALE_LOGO} style={{flex: 1, resizeMode: "contain"}}
style={{flex: 1, resizeMode: 'contain'}} resizeMode="contain"/>
resizeMode="contain" </View>
/> <Card style={{margin: 5}}>
</View> <Card.Title
<Card style={{margin: 5}}> title={i18n.t("screens.amicaleAbout.title")}
<Card.Title subtitle={i18n.t("screens.amicaleAbout.subtitle")}
title={i18n.t('screens.amicaleAbout.title')} left={props => <List.Icon {...props} icon={'information'}/>}
subtitle={i18n.t('screens.amicaleAbout.subtitle')} />
left={({ <Card.Content>
size, <Text>{i18n.t("screens.amicaleAbout.message")}</Text>
color, {/*$FlowFixMe*/}
}: { <FlatList
size: number, data={this.CONTACT_DATASET}
color: string, keyExtractor={this.keyExtractor}
}): React.Node => ( renderItem={this.renderItem}
<List.Icon size={size} color={color} icon="information" /> />
)} </Card.Content>
/> </Card>
<Card.Content> </View>
<Text>{i18n.t('screens.amicaleAbout.message')}</Text> );
<FlatList };
data={this.CONTACT_DATASET}
keyExtractor={this.keyExtractor} render() {
renderItem={this.getRenderItem} return (
<CollapsibleFlatList
data={[{key: "1"}]}
renderItem={this.getScreen}
hasTab={true}
/> />
</Card.Content> );
</Card> }
</View>
);
};
render(): React.Node {
return (
<CollapsibleFlatList
data={[{key: '1'}]}
renderItem={this.getScreen}
hasTab
/>
);
}
} }
export default withTheme(AmicaleContactScreen); export default withTheme(AmicaleContactScreen);

View file

@ -17,7 +17,7 @@ import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen
import CustomHTML from '../../../components/Overrides/CustomHTML'; import CustomHTML from '../../../components/Overrides/CustomHTML';
import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
import type {ClubCategoryType, ClubType} from './ClubListScreen'; import type {ClubCategoryType, ClubType} from './ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import {ERROR_TYPE} from '../../../utils/WebData'; import {ERROR_TYPE} from '../../../utils/WebData';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import type {ApiGenericDataType} from '../../../utils/WebData'; import type {ApiGenericDataType} from '../../../utils/WebData';
@ -32,7 +32,7 @@ type PropsType = {
}, },
... ...
}, },
theme: CustomThemeType, theme: CustomTheme,
}; };
const AMICALE_MAIL = 'clubs@amicale-insat.fr'; const AMICALE_MAIL = 'clubs@amicale-insat.fr';

View file

@ -11,7 +11,7 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
import {View} from 'react-native'; import {View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {DeviceType} from './EquipmentListScreen'; import type {DeviceType} from './EquipmentListScreen';
import {getRelativeDateString} from '../../../utils/EquipmentBooking'; import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
@ -23,7 +23,7 @@ type PropsType = {
dates: [string, string], dates: [string, string],
}, },
}, },
theme: CustomThemeType, theme: CustomTheme,
}; };
class EquipmentConfirmScreen extends React.Component<PropsType> { class EquipmentConfirmScreen extends React.Component<PropsType> {

View file

@ -15,7 +15,7 @@ import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {CalendarList} from 'react-native-calendars'; import {CalendarList} from 'react-native-calendars';
import type {DeviceType} from './EquipmentListScreen'; import type {DeviceType} from './EquipmentListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog'; import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import { import {
@ -36,7 +36,7 @@ type PropsType = {
item?: DeviceType, item?: DeviceType,
}, },
}, },
theme: CustomThemeType, theme: CustomTheme,
}; };
export type MarkedDatesObjectType = { export type MarkedDatesObjectType = {

View file

@ -1,446 +1,417 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native'; import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native";
import { import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
Button, import ConnectionManager from "../../managers/ConnectionManager";
Card,
HelperText,
TextInput,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import ErrorDialog from "../../components/Dialogs/ErrorDialog";
import LinearGradient from 'react-native-linear-gradient'; import type {CustomTheme} from "../../managers/ThemeManager";
import ConnectionManager from '../../managers/ConnectionManager'; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import ErrorDialog from '../../components/Dialogs/ErrorDialog'; import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomThemeType} from '../../managers/ThemeManager'; import AvailableWebsites from "../../constants/AvailableWebsites";
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import AvailableWebsites from '../../constants/AvailableWebsites'; import MascotPopup from "../../components/Mascot/MascotPopup";
import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import LinearGradient from "react-native-linear-gradient";
import MascotPopup from '../../components/Mascot/MascotPopup'; import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {nextScreen: string}}, route: { params: { nextScreen: string } },
theme: CustomThemeType, theme: CustomTheme
}; }
type StateType = { type State = {
email: string, email: string,
password: string, password: string,
isEmailValidated: boolean, isEmailValidated: boolean,
isPasswordValidated: boolean, isPasswordValidated: boolean,
loading: boolean, loading: boolean,
dialogVisible: boolean, dialogVisible: boolean,
dialogError: number, dialogError: number,
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
}; }
const ICON_AMICALE = require('../../../assets/amicale.png'); 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 = /^.+@.+\..+$/; const emailRegex = /^.+@.+\..+$/;
const styles = StyleSheet.create({ class LoginScreen extends React.Component<Props, State> {
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> { state = {
onEmailChange: (value: string) => void; email: '',
password: '',
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, isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: 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 = () => {
* Focuses the password field when the email field is done this.handleNavigationParams();
* };
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus();
};
/** /**
* Called when the user clicks on login or finishes to type his password. * Saves the screen to navigate to after a successful login if one was provided in navigation parameters
* */
* Checks if we should allow the user to login, handleNavigationParams() {
* then makes the login request and enters a loading state until the request finishes if (this.props.route.params != null) {
* if (this.props.route.params.nextScreen != null)
*/ this.nextScreen = this.props.route.params.nextScreen;
onSubmit = () => { else
const {email, password} = this.state; this.nextScreen = null;
if (this.shouldEnableLogin()) { }
this.setState({loading: true}); }
ConnectionManager.getInstance()
.connect(email, password) hideMascotDialog = () => {
.then(this.handleSuccess) AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.loginShowBanner.key, false);
.catch(this.showErrorDialog) this.setState({mascotDialogVisible: false})
.finally(() => { };
this.setState({loading: 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});
* 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 * Navigates to the screen specified in navigation parameters or simply go back tha stack.
* @returns {*} * Saves in user preferences to not show the login banner again.
*/ */
getMainCard(): React.Node { handleSuccess = () => {
const {props, state} = this; // Do not show the home login banner again
return ( AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.homeShowBanner.key, false);
<View style={styles.card}> if (this.nextScreen == null)
<Card.Title this.props.navigation.goBack();
title={i18n.t('screens.login.title')} else
titleStyle={{color: '#fff'}} this.props.navigation.replace(this.nextScreen);
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 * Navigates to the Amicale website screen with the reset password link as navigation parameters
*/ */
validateEmail = () => { onResetPasswordClick = () => this.props.navigation.navigate("website", {
this.setState({isEmailValidated: true}); host: AvailableWebsites.websites.AMICALE,
}; path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale')
/**
* 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}); * The user has unfocused the input, his email is ready to be validated
}; */
validateEmail = () => this.setState({isEmailValidated: true});
/** /**
* Navigates to the screen specified in navigation parameters or simply go back tha stack. * Checks if the entered email is valid (matches the regex)
* Saves in user preferences to not show the login banner again. *
*/ * @returns {boolean}
handleSuccess = () => { */
const {navigation} = this.props; isEmailValid() {
// Do not show the home login banner again return emailRegex.test(this.state.email);
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) * 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} *
*/ * @returns {boolean|boolean}
isEmailValid(): boolean { */
const {email} = this.state; shouldShowEmailError() {
return emailRegex.test(email); return this.state.isEmailValidated && !this.isEmailValid();
} }
/** /**
* Checks if we should tell the user his email is invalid. * The user has unfocused the input, his password is ready to be validated
* We should only show this if his email is invalid and has been checked when un-focusing the input */
* validatePassword = () => this.setState({isPasswordValidated: true});
* @returns {boolean|boolean}
*/
shouldShowEmailError(): boolean {
const {isEmailValidated} = this.state;
return isEmailValidated && !this.isEmailValid();
}
/** /**
* Checks if the user has entered a password * Checks if the user has entered a password
* *
* @returns {boolean} * @returns {boolean}
*/ */
isPasswordValid(): boolean { isPasswordValid() {
const {password} = this.state; return this.state.password !== '';
return password !== ''; }
}
/** /**
* Checks if we should tell the user his password is invalid. * 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 * We should only show this if his password is invalid and has been checked when un-focusing the input
* *
* @returns {boolean|boolean} * @returns {boolean|boolean}
*/ */
shouldShowPasswordError(): boolean { shouldShowPasswordError() {
const {isPasswordValidated} = this.state; return this.state.isPasswordValidated && !this.isPasswordValid();
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 * If the email and password are valid, and we are not loading a request, then the login button can be enabled
* *
* @returns {boolean} * @returns {boolean}
*/ */
shouldEnableLogin(): boolean { shouldEnableLogin() {
const {loading} = this.state; return this.isEmailValid() && this.isPasswordValid() && !this.state.loading;
return this.isEmailValid() && this.isPasswordValid() && !loading; }
}
render(): React.Node { /**
const {mascotDialogVisible, dialogVisible, dialogError} = this.state; * Called when the user input changes in the email or password field.
return ( * This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
<LinearGradient *
style={{ * @param isEmail True if the field is the email field
height: '100%', * @param value The new field value
}} */
colors={['#9e0d18', '#530209']} onInputChange(isEmail: boolean, value: string) {
start={{x: 0, y: 0.1}} if (isEmail) {
end={{x: 0.1, y: 1}}> this.setState({
<KeyboardAvoidingView email: value,
behavior="height" isEmailValidated: false,
contentContainerStyle={styles.container} });
style={styles.container} } else {
enabled this.setState({
keyboardVerticalOffset={100}> password: value,
<CollapsibleScrollView> isPasswordValidated: false,
<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" * Focuses the password field when the email field is done
buttons={{ *
action: null, * @returns {*}
cancel: { */
message: i18n.t('screens.login.mascotDialog.button'), onEmailSubmit = () => {
icon: 'check', if (this.passwordInputRef.current != null)
onPress: this.hideMascotDialog, this.passwordInputRef.current.focus();
}, }
}}
emotion={MASCOT_STYLE.NORMAL} /**
/> * Called when the user clicks on login or finishes to type his password.
<ErrorDialog *
visible={dialogVisible} * Checks if we should allow the user to login,
onDismiss={this.hideErrorDialog} * then makes the login request and enters a loading state until the request finishes
errorCode={dialogError} *
/> */
</CollapsibleScrollView> onSubmit = () => {
</KeyboardAvoidingView> if (this.shouldEnableLogin()) {
</LinearGradient> 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,
}
});
export default withTheme(LoginScreen); export default withTheme(LoginScreen);

View file

@ -1,467 +1,432 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from "react-native";
import { import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
Avatar, import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
Button,
Card,
Divider,
List,
Paragraph,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import LogoutDialog from "../../components/Amicale/LogoutDialog";
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import type {cardList} from "../../components/Lists/CardList/CardList";
import MaterialHeaderButtons, { import CardList from "../../components/Lists/CardList/CardList";
Item, import {StackNavigationProp} from "@react-navigation/stack";
} from '../../components/Overrides/CustomHeaderButton'; import type {CustomTheme} from "../../managers/ThemeManager";
import CardList from '../../components/Lists/CardList/CardList'; import AvailableWebsites from "../../constants/AvailableWebsites";
import type {CustomThemeType} from '../../managers/ThemeManager'; import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import AvailableWebsites from '../../constants/AvailableWebsites'; import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ServiceItemType} from '../../managers/ServicesManager';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
};
type StateType = {
dialogVisible: boolean,
};
type ClubType = {
id: number,
name: string,
is_manager: boolean,
};
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',
},
});
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}
/>
);
}
} }
type State = {
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,
}
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}
/>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
editButton: {
marginLeft: 'auto'
}
});
export default withTheme(ProfileScreen); export default withTheme(ProfileScreen);

View file

@ -1,13 +1,13 @@
// @flow // @flow
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
export type CoordinatesType = { export type Coordinates = {
x: number, x: number,
y: number, y: number,
}; }
export type ShapeType = Array<Array<number>>; type Shape = Array<Array<number>>;
/** /**
* Abstract class used to represent a BaseShape. * Abstract class used to represent a BaseShape.
@ -15,98 +15,96 @@ export type ShapeType = Array<Array<number>>;
* and in methods to implement * and in methods to implement
*/ */
export default class BaseShape { export default class BaseShape {
#currentShape: ShapeType;
#rotation: number; #currentShape: Shape;
#rotation: number;
position: Coordinates;
theme: CustomTheme;
position: CoordinatesType; /**
* Prevent instantiation if classname is BaseShape to force class to be abstract
theme: CustomThemeType; */
constructor(theme: CustomTheme) {
/** if (this.constructor === BaseShape)
* Prevent instantiation if classname is BaseShape to force class to be abstract throw new Error("Abstract class can't be instantiated");
*/ this.theme = theme;
constructor(theme: CustomThemeType) { this.#rotation = 0;
if (this.constructor === BaseShape) this.position = {x: 0, y: 0};
throw new Error("Abstract class can't be instantiated"); this.#currentShape = this.getShapes()[this.#rotation];
this.theme = theme;
this.#rotation = 0;
this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation];
}
/**
* 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 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;
}
/** /**
* Rotate this object * Gets this shape's color.
* * Must be implemented by child class
* @param isForward Should we rotate clockwise? */
*/ getColor(): string {
rotate(isForward: boolean) { throw new Error("Method 'getColor()' must be implemented");
if (isForward) this.#rotation += 1; }
else this.#rotation -= 1;
if (this.#rotation > 3) this.#rotation = 0; /**
else if (this.#rotation < 0) this.#rotation = 3; * Gets this object's all possible shapes as an array.
this.#currentShape = this.getShapes()[this.#rotation]; * Must be implemented by child class.
} *
* Used by tests to read private fields
*/
getShapes(): Array<Shape> {
throw new Error("Method 'getShapes()' must be implemented");
}
/**
* Gets this object's current shape.
*/
getCurrentShape(): Shape {
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<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});
}
}
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;
}
/**
* 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;
}
} }

View file

@ -1,46 +1,45 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeI extends BaseShape { export default class ShapeI extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisI; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisI;
return [ }
[
[0, 0, 0, 0], getShapes() {
[1, 1, 1, 1], return [
[0, 0, 0, 0], [
[0, 0, 0, 0], [0, 0, 0, 0],
], [1, 1, 1, 1],
[ [0, 0, 0, 0],
[0, 0, 1, 0], [0, 0, 0, 0],
[0, 0, 1, 0], ],
[0, 0, 1, 0], [
[0, 0, 1, 0], [0, 0, 1, 0],
], [0, 0, 1, 0],
[ [0, 0, 1, 0],
[0, 0, 0, 0], [0, 0, 1, 0],
[0, 0, 0, 0], ],
[1, 1, 1, 1], [
[0, 0, 0, 0], [0, 0, 0, 0],
], [0, 0, 0, 0],
[ [1, 1, 1, 1],
[0, 1, 0, 0], [0, 0, 0, 0],
[0, 1, 0, 0], ],
[0, 1, 0, 0], [
[0, 1, 0, 0], [0, 1, 0, 0],
], [0, 1, 0, 0],
]; [0, 1, 0, 0],
} [0, 1, 0, 0],
],
];
}
} }

View file

@ -1,42 +1,41 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeJ extends BaseShape { export default class ShapeJ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisJ; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisJ;
return [ }
[
[1, 0, 0], getShapes() {
[1, 1, 1], return [
[0, 0, 0], [
], [1, 0, 0],
[ [1, 1, 1],
[0, 1, 1], [0, 0, 0],
[0, 1, 0], ],
[0, 1, 0], [
], [0, 1, 1],
[ [0, 1, 0],
[0, 0, 0], [0, 1, 0],
[1, 1, 1], ],
[0, 0, 1], [
], [0, 0, 0],
[ [1, 1, 1],
[0, 1, 0], [0, 0, 1],
[0, 1, 0], ],
[1, 1, 0], [
], [0, 1, 0],
]; [0, 1, 0],
} [1, 1, 0],
],
];
}
} }

View file

@ -1,42 +1,41 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeL extends BaseShape { export default class ShapeL extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisL; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisL;
return [ }
[
[0, 0, 1], getShapes() {
[1, 1, 1], return [
[0, 0, 0], [
], [0, 0, 1],
[ [1, 1, 1],
[0, 1, 0], [0, 0, 0],
[0, 1, 0], ],
[0, 1, 1], [
], [0, 1, 0],
[ [0, 1, 0],
[0, 0, 0], [0, 1, 1],
[1, 1, 1], ],
[1, 0, 0], [
], [0, 0, 0],
[ [1, 1, 1],
[1, 1, 0], [1, 0, 0],
[0, 1, 0], ],
[0, 1, 0], [
], [1, 1, 0],
]; [0, 1, 0],
} [0, 1, 0],
],
];
}
} }

View file

@ -1,38 +1,37 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeO extends BaseShape { export default class ShapeO extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 4;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisO; super(theme);
} this.position.x = 4;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisO;
return [ }
[
[1, 1], getShapes() {
[1, 1], return [
], [
[ [1, 1],
[1, 1], [1, 1],
[1, 1], ],
], [
[ [1, 1],
[1, 1], [1, 1],
[1, 1], ],
], [
[ [1, 1],
[1, 1], [1, 1],
[1, 1], ],
], [
]; [1, 1],
} [1, 1],
],
];
}
} }

View file

@ -1,42 +1,41 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeS extends BaseShape { export default class ShapeS extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisS; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisS;
return [ }
[
[0, 1, 1], getShapes() {
[1, 1, 0], return [
[0, 0, 0], [
], [0, 1, 1],
[ [1, 1, 0],
[0, 1, 0], [0, 0, 0],
[0, 1, 1], ],
[0, 0, 1], [
], [0, 1, 0],
[ [0, 1, 1],
[0, 0, 0], [0, 0, 1],
[0, 1, 1], ],
[1, 1, 0], [
], [0, 0, 0],
[ [0, 1, 1],
[1, 0, 0], [1, 1, 0],
[1, 1, 0], ],
[0, 1, 0], [
], [1, 0, 0],
]; [1, 1, 0],
} [0, 1, 0],
],
];
}
} }

View file

@ -1,42 +1,41 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeT extends BaseShape { export default class ShapeT extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisT; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisT;
return [ }
[
[0, 1, 0], getShapes() {
[1, 1, 1], return [
[0, 0, 0], [
], [0, 1, 0],
[ [1, 1, 1],
[0, 1, 0], [0, 0, 0],
[0, 1, 1], ],
[0, 1, 0], [
], [0, 1, 0],
[ [0, 1, 1],
[0, 0, 0], [0, 1, 0],
[1, 1, 1], ],
[0, 1, 0], [
], [0, 0, 0],
[ [1, 1, 1],
[0, 1, 0], [0, 1, 0],
[1, 1, 0], ],
[0, 1, 0], [
], [0, 1, 0],
]; [1, 1, 0],
} [0, 1, 0],
],
];
}
} }

View file

@ -1,42 +1,41 @@
// @flow // @flow
import BaseShape from './BaseShape'; import BaseShape from "./BaseShape";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {ShapeType} from './BaseShape';
export default class ShapeZ extends BaseShape { export default class ShapeZ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
getColor(): string { constructor(theme: CustomTheme) {
return this.theme.colors.tetrisZ; super(theme);
} this.position.x = 3;
}
// eslint-disable-next-line class-methods-use-this getColor(): string {
getShapes(): Array<ShapeType> { return this.theme.colors.tetrisZ;
return [ }
[
[1, 1, 0], getShapes() {
[0, 1, 1], return [
[0, 0, 0], [
], [1, 1, 0],
[ [0, 1, 1],
[0, 0, 1], [0, 0, 0],
[0, 1, 1], ],
[0, 1, 0], [
], [0, 0, 1],
[ [0, 1, 1],
[0, 0, 0], [0, 1, 0],
[1, 1, 0], ],
[0, 1, 1], [
], [0, 0, 0],
[ [1, 1, 0],
[0, 1, 0], [0, 1, 1],
[1, 1, 0], ],
[1, 0, 0], [
], [0, 1, 0],
]; [1, 1, 0],
} [1, 0, 0],
],
];
}
} }

View file

@ -1,106 +1,103 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import GridManager from '../logic/GridManager'; import GridManager from "../logic/GridManager";
import ScoreManager from '../logic/ScoreManager'; import ScoreManager from "../logic/ScoreManager";
import Piece from '../logic/Piece'; import Piece from "../logic/Piece";
let colors = { let colors = {
tetrisBackground: '#000002', tetrisBackground: "#000002"
}; };
jest.mock('../ScoreManager'); jest.mock("../ScoreManager");
afterAll(() => { 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},
]);
expect(g.getEmptyLine(-1)).toStrictEqual([]); test('getEmptyLine', () => {
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([]);
}); });
test('getEmptyGrid', () => { test('getEmptyGrid', () => {
let g = new GridManager(2, 2, colors); let g = new GridManager(2, 2, colors);
expect(g.getEmptyGrid(2, 2)).toStrictEqual([ 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},
], ],
[ [
{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(-1, 2)).toStrictEqual([]);
expect(g.getEmptyGrid(2, -1)).toStrictEqual([[], []]); expect(g.getEmptyGrid(2, -1)).toStrictEqual([[], []]);
}); });
test('getLinesToClear', () => { test('getLinesToClear', () => {
let g = new GridManager(2, 2, colors); let g = new GridManager(2, 2, colors);
g.getCurrentGrid()[0][0].isEmpty = false; g.getCurrentGrid()[0][0].isEmpty = false;
g.getCurrentGrid()[0][1].isEmpty = false; g.getCurrentGrid()[0][1].isEmpty = false;
let coord = [{x: 1, y: 0}]; let coord = [{x: 1, y: 0}];
expect(g.getLinesToClear(coord)).toStrictEqual([0]); expect(g.getLinesToClear(coord)).toStrictEqual([0]);
g.getCurrentGrid()[0][0].isEmpty = true; g.getCurrentGrid()[0][0].isEmpty = true;
g.getCurrentGrid()[0][1].isEmpty = true; g.getCurrentGrid()[0][1].isEmpty = true;
g.getCurrentGrid()[1][0].isEmpty = false; g.getCurrentGrid()[1][0].isEmpty = false;
g.getCurrentGrid()[1][1].isEmpty = false; g.getCurrentGrid()[1][1].isEmpty = false;
expect(g.getLinesToClear(coord)).toStrictEqual([]); expect(g.getLinesToClear(coord)).toStrictEqual([]);
coord = [{x: 1, y: 1}]; coord = [{x: 1, y: 1}];
expect(g.getLinesToClear(coord)).toStrictEqual([1]); expect(g.getLinesToClear(coord)).toStrictEqual([1]);
}); });
test('clearLines', () => { test('clearLines', () => {
let g = new GridManager(2, 2, colors); let g = new GridManager(2, 2, colors);
let grid = [ let grid = [
[ [
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
], ],
[ [
{color: '0', isEmpty: true}, {color: '0', isEmpty: true},
{color: '0', isEmpty: true}, {color: '0', isEmpty: true},
], ],
]; ];
g.getCurrentGrid()[1][0].color = '0'; g.getCurrentGrid()[1][0].color = '0';
g.getCurrentGrid()[1][1].color = '0'; g.getCurrentGrid()[1][1].color = '0';
expect(g.getCurrentGrid()).toStrictEqual(grid); expect(g.getCurrentGrid()).toStrictEqual(grid);
let scoreManager = new ScoreManager(); let scoreManager = new ScoreManager();
g.clearLines([1], scoreManager); g.clearLines([1], scoreManager);
grid = [ grid = [
[ [
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
], ],
[ [
{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); expect(g.getCurrentGrid()).toStrictEqual(grid);
}); });
test('freezeTetromino', () => { test('freezeTetromino', () => {
let g = new GridManager(2, 2, colors); let g = new GridManager(2, 2, colors);
let spy1 = jest let spy1 = jest.spyOn(GridManager.prototype, 'getLinesToClear')
.spyOn(GridManager.prototype, 'getLinesToClear') .mockImplementation(() => {});
.mockImplementation(() => {}); let spy2 = jest.spyOn(GridManager.prototype, 'clearLines')
let spy2 = jest .mockImplementation(() => {});
.spyOn(GridManager.prototype, 'clearLines') g.freezeTetromino(new Piece({}), null);
.mockImplementation(() => {});
g.freezeTetromino(new Piece({}), null);
expect(spy1).toHaveBeenCalled(); expect(spy1).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled(); expect(spy2).toHaveBeenCalled();
spy1.mockRestore(); spy1.mockRestore();
spy2.mockRestore(); spy2.mockRestore();
}); });

View file

@ -1,186 +1,155 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import Piece from '../logic/Piece'; import Piece from "../logic/Piece";
import ShapeI from '../Shapes/ShapeI'; import ShapeI from "../Shapes/ShapeI";
let colors = { let colors = {
tetrisI: '#000001', tetrisI: "#000001",
tetrisBackground: '#000002', tetrisBackground: "#000002"
}; };
jest.mock('../Shapes/ShapeI'); jest.mock("../Shapes/ShapeI");
beforeAll(() => { beforeAll(() => {
jest jest.spyOn(Piece.prototype, 'getRandomShape')
.spyOn(Piece.prototype, 'getRandomShape') .mockImplementation((colors: Object) => {return new ShapeI(colors);});
.mockImplementation((colors: Object) => {
return new ShapeI(colors);
});
}); });
afterAll(() => { afterAll(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
test('isPositionValid', () => { test('isPositionValid', () => {
let x = 0; let x = 0;
let y = 0; let y = 0;
let spy = jest let spy = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
.spyOn(ShapeI.prototype, 'getCellsCoordinates') .mockImplementation(() => {return [{x: x, y: y}];});
.mockImplementation(() => { let grid = [
return [{x: x, y: y}]; [{isEmpty: true}, {isEmpty: true}],
}); [{isEmpty: true}, {isEmpty: false}],
let grid = [ ];
[{isEmpty: true}, {isEmpty: true}], let size = 2;
[{isEmpty: true}, {isEmpty: false}],
];
let size = 2;
let p = new Piece(colors); let p = new Piece(colors);
expect(p.isPositionValid(grid, size, size)).toBeTrue(); expect(p.isPositionValid(grid, size, size)).toBeTrue();
x = 1; x = 1; y = 0;
y = 0; expect(p.isPositionValid(grid, size, size)).toBeTrue();
expect(p.isPositionValid(grid, size, size)).toBeTrue(); x = 0; y = 1;
x = 0; expect(p.isPositionValid(grid, size, size)).toBeTrue();
y = 1; x = 1; y = 1;
expect(p.isPositionValid(grid, size, size)).toBeTrue(); expect(p.isPositionValid(grid, size, size)).toBeFalse();
x = 1; x = 2; y = 0;
y = 1; expect(p.isPositionValid(grid, size, size)).toBeFalse();
expect(p.isPositionValid(grid, size, size)).toBeFalse(); x = -1; y = 0;
x = 2; expect(p.isPositionValid(grid, size, size)).toBeFalse();
y = 0; x = 0; y = 2;
expect(p.isPositionValid(grid, size, size)).toBeFalse(); expect(p.isPositionValid(grid, size, size)).toBeFalse();
x = -1; x = 0; y = -1;
y = 0; expect(p.isPositionValid(grid, size, size)).toBeFalse();
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', () => { test('tryMove', () => {
let p = new Piece(colors); let p = new Piece(colors);
const callbackMock = jest.fn(); const callbackMock = jest.fn();
let isValid = true; let isValid = true;
let spy1 = jest let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid')
.spyOn(Piece.prototype, 'isPositionValid') .mockImplementation(() => {return isValid;});
.mockImplementation(() => { let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid')
return isValid; .mockImplementation(() => {});
}); let spy3 = jest.spyOn(Piece.prototype, 'toGrid')
let spy2 = jest .mockImplementation(() => {});
.spyOn(Piece.prototype, 'removeFromGrid')
.mockImplementation(() => {});
let spy3 = jest.spyOn(Piece.prototype, 'toGrid').mockImplementation(() => {});
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue(); expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeTrue();
isValid = false; isValid = false;
expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeFalse(); expect(p.tryMove(-1, 0, null, null, null, callbackMock)).toBeFalse();
isValid = true; isValid = true;
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeTrue(); expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeTrue();
expect(callbackMock).toBeCalledTimes(0); expect(callbackMock).toBeCalledTimes(0);
isValid = false; isValid = false;
expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse(); expect(p.tryMove(0, 1, null, null, null, callbackMock)).toBeFalse();
expect(callbackMock).toBeCalledTimes(1); expect(callbackMock).toBeCalledTimes(1);
expect(spy2).toBeCalledTimes(4); expect(spy2).toBeCalledTimes(4);
expect(spy3).toBeCalledTimes(4); expect(spy3).toBeCalledTimes(4);
spy1.mockRestore(); spy1.mockRestore();
spy2.mockRestore(); spy2.mockRestore();
spy3.mockRestore(); spy3.mockRestore();
}); });
test('tryRotate', () => { test('tryRotate', () => {
let p = new Piece(colors); let p = new Piece(colors);
let isValid = true; let isValid = true;
let spy1 = jest let spy1 = jest.spyOn(Piece.prototype, 'isPositionValid')
.spyOn(Piece.prototype, 'isPositionValid') .mockImplementation(() => {return isValid;});
.mockImplementation(() => { let spy2 = jest.spyOn(Piece.prototype, 'removeFromGrid')
return isValid; .mockImplementation(() => {});
}); let spy3 = jest.spyOn(Piece.prototype, 'toGrid')
let spy2 = jest .mockImplementation(() => {});
.spyOn(Piece.prototype, 'removeFromGrid')
.mockImplementation(() => {});
let spy3 = jest.spyOn(Piece.prototype, 'toGrid').mockImplementation(() => {});
expect(p.tryRotate(null, null, null)).toBeTrue(); expect(p.tryRotate( null, null, null)).toBeTrue();
isValid = false; isValid = false;
expect(p.tryRotate(null, null, null)).toBeFalse(); expect(p.tryRotate( null, null, null)).toBeFalse();
expect(spy2).toBeCalledTimes(2); expect(spy2).toBeCalledTimes(2);
expect(spy3).toBeCalledTimes(2); expect(spy3).toBeCalledTimes(2);
spy1.mockRestore(); spy1.mockRestore();
spy2.mockRestore(); spy2.mockRestore();
spy3.mockRestore(); spy3.mockRestore();
}); });
test('toGrid', () => { test('toGrid', () => {
let x = 0; let x = 0;
let y = 0; let y = 0;
let spy1 = jest let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
.spyOn(ShapeI.prototype, 'getCellsCoordinates') .mockImplementation(() => {return [{x: x, y: y}];});
.mockImplementation(() => { let spy2 = jest.spyOn(ShapeI.prototype, 'getColor')
return [{x: x, y: y}]; .mockImplementation(() => {return colors.tetrisI;});
}); let grid = [
let spy2 = jest.spyOn(ShapeI.prototype, 'getColor').mockImplementation(() => { [{isEmpty: true}, {isEmpty: true}],
return colors.tetrisI; [{isEmpty: true}, {isEmpty: true}],
}); ];
let grid = [ let expectedGrid = [
[{isEmpty: true}, {isEmpty: true}], [{color: colors.tetrisI, isEmpty: false}, {isEmpty: true}],
[{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); let p = new Piece(colors);
p.toGrid(grid, true); p.toGrid(grid, true);
expect(grid).toStrictEqual(expectedGrid); expect(grid).toStrictEqual(expectedGrid);
spy1.mockRestore(); spy1.mockRestore();
spy2.mockRestore(); spy2.mockRestore();
}); });
test('removeFromGrid', () => { test('removeFromGrid', () => {
let gridOld = [ let gridOld = [
[ [
{color: colors.tetrisI, isEmpty: false}, {color: colors.tetrisI, isEmpty: false},
{color: colors.tetrisI, isEmpty: false}, {color: colors.tetrisI, isEmpty: false},
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
], ],
]; ];
let gridNew = [ let gridNew = [
[ [
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
{color: colors.tetrisBackground, isEmpty: true}, {color: colors.tetrisBackground, isEmpty: true},
], ],
]; ];
let oldCoord = [ let oldCoord = [{x: 0, y: 0}, {x: 1, y: 0}];
{x: 0, y: 0}, let spy1 = jest.spyOn(ShapeI.prototype, 'getCellsCoordinates')
{x: 1, y: 0}, .mockImplementation(() => {return oldCoord;});
]; let spy2 = jest.spyOn(ShapeI.prototype, 'getColor')
let spy1 = jest .mockImplementation(() => {return colors.tetrisI;});
.spyOn(ShapeI.prototype, 'getCellsCoordinates') let p = new Piece(colors);
.mockImplementation(() => { p.removeFromGrid(gridOld);
return oldCoord; expect(gridOld).toStrictEqual(gridNew);
});
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(); spy1.mockRestore();
spy2.mockRestore(); spy2.mockRestore();
}); });

View file

@ -1,72 +1,71 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import ScoreManager from '../logic/ScoreManager'; import ScoreManager from "../logic/ScoreManager";
test('incrementScore', () => { test('incrementScore', () => {
let scoreManager = new ScoreManager(); let scoreManager = new ScoreManager();
expect(scoreManager.getScore()).toBe(0); expect(scoreManager.getScore()).toBe(0);
scoreManager.incrementScore(); scoreManager.incrementScore();
expect(scoreManager.getScore()).toBe(1); expect(scoreManager.getScore()).toBe(1);
}); });
test('addLinesRemovedPoints', () => { test('addLinesRemovedPoints', () => {
let scoreManager = new ScoreManager(); let scoreManager = new ScoreManager();
scoreManager.addLinesRemovedPoints(0); scoreManager.addLinesRemovedPoints(0);
scoreManager.addLinesRemovedPoints(5); scoreManager.addLinesRemovedPoints(5);
expect(scoreManager.getScore()).toBe(0); expect(scoreManager.getScore()).toBe(0);
expect(scoreManager.getLevelProgression()).toBe(0); expect(scoreManager.getLevelProgression()).toBe(0);
scoreManager.addLinesRemovedPoints(1); scoreManager.addLinesRemovedPoints(1);
expect(scoreManager.getScore()).toBe(40); expect(scoreManager.getScore()).toBe(40);
expect(scoreManager.getLevelProgression()).toBe(1); expect(scoreManager.getLevelProgression()).toBe(1);
scoreManager.addLinesRemovedPoints(2); scoreManager.addLinesRemovedPoints(2);
expect(scoreManager.getScore()).toBe(140); expect(scoreManager.getScore()).toBe(140);
expect(scoreManager.getLevelProgression()).toBe(4); expect(scoreManager.getLevelProgression()).toBe(4);
scoreManager.addLinesRemovedPoints(3); scoreManager.addLinesRemovedPoints(3);
expect(scoreManager.getScore()).toBe(440); expect(scoreManager.getScore()).toBe(440);
expect(scoreManager.getLevelProgression()).toBe(9); expect(scoreManager.getLevelProgression()).toBe(9);
scoreManager.addLinesRemovedPoints(4); scoreManager.addLinesRemovedPoints(4);
expect(scoreManager.getScore()).toBe(1640); expect(scoreManager.getScore()).toBe(1640);
expect(scoreManager.getLevelProgression()).toBe(17); expect(scoreManager.getLevelProgression()).toBe(17);
}); });
test('canLevelUp', () => { test('canLevelUp', () => {
let scoreManager = new ScoreManager(); let scoreManager = new ScoreManager();
expect(scoreManager.canLevelUp()).toBeFalse(); expect(scoreManager.canLevelUp()).toBeFalse();
expect(scoreManager.getLevel()).toBe(0); expect(scoreManager.getLevel()).toBe(0);
expect(scoreManager.getLevelProgression()).toBe(0); expect(scoreManager.getLevelProgression()).toBe(0);
scoreManager.addLinesRemovedPoints(1); scoreManager.addLinesRemovedPoints(1);
expect(scoreManager.canLevelUp()).toBeTrue(); expect(scoreManager.canLevelUp()).toBeTrue();
expect(scoreManager.getLevel()).toBe(1); expect(scoreManager.getLevel()).toBe(1);
expect(scoreManager.getLevelProgression()).toBe(1); expect(scoreManager.getLevelProgression()).toBe(1);
scoreManager.addLinesRemovedPoints(1); scoreManager.addLinesRemovedPoints(1);
expect(scoreManager.canLevelUp()).toBeFalse(); expect(scoreManager.canLevelUp()).toBeFalse();
expect(scoreManager.getLevel()).toBe(1); expect(scoreManager.getLevel()).toBe(1);
expect(scoreManager.getLevelProgression()).toBe(2); expect(scoreManager.getLevelProgression()).toBe(2);
scoreManager.addLinesRemovedPoints(2); scoreManager.addLinesRemovedPoints(2);
expect(scoreManager.canLevelUp()).toBeFalse(); expect(scoreManager.canLevelUp()).toBeFalse();
expect(scoreManager.getLevel()).toBe(1); expect(scoreManager.getLevel()).toBe(1);
expect(scoreManager.getLevelProgression()).toBe(5); expect(scoreManager.getLevelProgression()).toBe(5);
scoreManager.addLinesRemovedPoints(1); scoreManager.addLinesRemovedPoints(1);
expect(scoreManager.canLevelUp()).toBeTrue(); expect(scoreManager.canLevelUp()).toBeTrue();
expect(scoreManager.getLevel()).toBe(2); expect(scoreManager.getLevel()).toBe(2);
expect(scoreManager.getLevelProgression()).toBe(1); expect(scoreManager.getLevelProgression()).toBe(1);
scoreManager.addLinesRemovedPoints(4); scoreManager.addLinesRemovedPoints(4);
expect(scoreManager.canLevelUp()).toBeFalse(); expect(scoreManager.canLevelUp()).toBeFalse();
expect(scoreManager.getLevel()).toBe(2); expect(scoreManager.getLevel()).toBe(2);
expect(scoreManager.getLevelProgression()).toBe(9); expect(scoreManager.getLevelProgression()).toBe(9);
scoreManager.addLinesRemovedPoints(2); scoreManager.addLinesRemovedPoints(2);
expect(scoreManager.canLevelUp()).toBeTrue(); expect(scoreManager.canLevelUp()).toBeTrue();
expect(scoreManager.getLevel()).toBe(3); expect(scoreManager.getLevel()).toBe(3);
expect(scoreManager.getLevelProgression()).toBe(2); expect(scoreManager.getLevelProgression()).toBe(2);
}); });

View file

@ -1,106 +1,104 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import BaseShape from '../Shapes/BaseShape'; import BaseShape from "../Shapes/BaseShape";
import ShapeI from '../Shapes/ShapeI'; import ShapeI from "../Shapes/ShapeI";
const colors = { const colors = {
tetrisI: '#000001', tetrisI: '#000001',
tetrisO: '#000002', tetrisO: '#000002',
tetrisT: '#000003', tetrisT: '#000003',
tetrisS: '#000004', tetrisS: '#000004',
tetrisZ: '#000005', tetrisZ: '#000005',
tetrisJ: '#000006', tetrisJ: '#000006',
tetrisL: '#000007', tetrisL: '#000007',
}; };
test('constructor', () => { test('constructor', () => {
expect(() => new BaseShape()).toThrow(Error); expect(() => new BaseShape()).toThrow(Error);
let T = new ShapeI(colors); let T = new ShapeI(colors);
expect(T.position.y).toBe(0); expect(T.position.y).toBe(0);
expect(T.position.x).toBe(3); expect(T.position.x).toBe(3);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
expect(T.getColor()).toBe(colors.tetrisI); expect(T.getColor()).toBe(colors.tetrisI);
}); });
test('move', () => { test("move", () => {
let T = new ShapeI(colors); let T = new ShapeI(colors);
T.move(0, 1); T.move(0, 1);
expect(T.position.x).toBe(3); expect(T.position.x).toBe(3);
expect(T.position.y).toBe(1); expect(T.position.y).toBe(1);
T.move(1, 0); T.move(1, 0);
expect(T.position.x).toBe(4); expect(T.position.x).toBe(4);
expect(T.position.y).toBe(1); expect(T.position.y).toBe(1);
T.move(1, 1); T.move(1, 1);
expect(T.position.x).toBe(5); expect(T.position.x).toBe(5);
expect(T.position.y).toBe(2); expect(T.position.y).toBe(2);
T.move(2, 2); T.move(2, 2);
expect(T.position.x).toBe(7); expect(T.position.x).toBe(7);
expect(T.position.y).toBe(4); expect(T.position.y).toBe(4);
T.move(-1, -1); T.move(-1, -1);
expect(T.position.x).toBe(6); expect(T.position.x).toBe(6);
expect(T.position.y).toBe(3); expect(T.position.y).toBe(3);
}); });
test('rotate', () => { test('rotate', () => {
let T = new ShapeI(colors); let T = new ShapeI(colors);
T.rotate(true); T.rotate(true);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
T.rotate(true); T.rotate(true);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
T.rotate(true); T.rotate(true);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
T.rotate(true); T.rotate(true);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
T.rotate(false); T.rotate(false);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[3]);
T.rotate(false); T.rotate(false);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[2]);
T.rotate(false); T.rotate(false);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[1]);
T.rotate(false); T.rotate(false);
expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]); expect(T.getCurrentShape()).toStrictEqual(T.getShapes()[0]);
}); });
test('getCellsCoordinates', () => { test('getCellsCoordinates', () => {
let T = new ShapeI(colors); let T = new ShapeI(colors);
expect(T.getCellsCoordinates(false)).toStrictEqual([ expect(T.getCellsCoordinates(false)).toStrictEqual([
{x: 0, y: 1}, {x: 0, y: 1},
{x: 1, y: 1}, {x: 1, y: 1},
{x: 2, y: 1}, {x: 2, y: 1},
{x: 3, y: 1}, {x: 3, y: 1},
]); ]);
expect(T.getCellsCoordinates(true)).toStrictEqual([ expect(T.getCellsCoordinates(true)).toStrictEqual([
{x: 3, y: 1}, {x: 3, y: 1},
{x: 4, y: 1}, {x: 4, y: 1},
{x: 5, y: 1}, {x: 5, y: 1},
{x: 6, y: 1}, {x: 6, y: 1},
]); ]);
T.move(1, 1); T.move(1, 1);
expect(T.getCellsCoordinates(false)).toStrictEqual([ expect(T.getCellsCoordinates(false)).toStrictEqual([
{x: 0, y: 1}, {x: 0, y: 1},
{x: 1, y: 1}, {x: 1, y: 1},
{x: 2, y: 1}, {x: 2, y: 1},
{x: 3, y: 1}, {x: 3, y: 1},
]); ]);
expect(T.getCellsCoordinates(true)).toStrictEqual([ expect(T.getCellsCoordinates(true)).toStrictEqual([
{x: 4, y: 2}, {x: 4, y: 2},
{x: 5, y: 2}, {x: 5, y: 2},
{x: 6, y: 2}, {x: 6, y: 2},
{x: 7, y: 2}, {x: 7, y: 2},
]); ]);
T.rotate(true); T.rotate(true);
expect(T.getCellsCoordinates(false)).toStrictEqual([ expect(T.getCellsCoordinates(false)).toStrictEqual([
{x: 2, y: 0}, {x: 2, y: 0},
{x: 2, y: 1}, {x: 2, y: 1},
{x: 2, y: 2}, {x: 2, y: 2},
{x: 2, y: 3}, {x: 2, y: 3},
]); ]);
expect(T.getCellsCoordinates(true)).toStrictEqual([ expect(T.getCellsCoordinates(true)).toStrictEqual([
{x: 6, y: 1}, {x: 6, y: 1},
{x: 6, y: 2}, {x: 6, y: 2},
{x: 6, y: 3}, {x: 6, y: 3},
{x: 6, y: 4}, {x: 6, y: 4},
]); ]);
}); });

View file

@ -3,30 +3,34 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager";
export type CellType = {color: string, isEmpty: boolean, key: string}; 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,
}}
/>
);
}
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); export default withTheme(CellComponent);

View file

@ -3,55 +3,56 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type {Cell} from "./CellComponent";
import type {CellType} from './CellComponent'; import CellComponent from "./CellComponent";
import CellComponent from './CellComponent'; import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
export type GridType = Array<Array<CellComponent>>; export type Grid = Array<Array<CellComponent>>;
type PropsType = { type Props = {
grid: Array<Array<CellType>>, grid: Array<Array<Object>>,
height: number, height: number,
width: number, width: number,
style: ViewStyle, style: ViewStyle,
}; }
class GridComponent extends React.Component<PropsType> { class GridComponent extends React.Component<Props> {
getRow(rowNumber: number): React.Node {
const {grid} = this.props;
return (
<View style={{flexDirection: 'row'}} key={rowNumber.toString()}>
{grid[rowNumber].map(this.getCellRender)}
</View>
);
}
getCellRender = (item: CellType): React.Node => { getRow(rowNumber: number) {
return <CellComponent cell={item} key={item.key} />; let cells = this.props.grid[rowNumber].map(this.getCellRender);
}; return (
<View
getGrid(): React.Node { style={{flexDirection: 'row',}}
const {height} = this.props; key={rowNumber.toString()}
const rows = []; >
for (let i = 0; i < height; i += 1) { {cells}
rows.push(this.getRow(i)); </View>
);
} }
return rows;
}
render(): React.Node { getCellRender = (item: Cell) => {
const {style, width, height} = this.props; return <CellComponent cell={item} key={item.key}/>;
return ( };
<View
style={{ getGrid() {
aspectRatio: width / height, let rows = [];
borderRadius: 4, for (let i = 0; i < this.props.height; i++) {
...style, rows.push(this.getRow(i));
}}> }
{this.getGrid()} return rows;
</View> }
);
} render() {
return (
<View style={{
aspectRatio: this.props.width / this.props.height,
borderRadius: 4,
...this.props.style
}}>
{this.getGrid()}
</View>
);
}
} }
export default withTheme(GridComponent); export default withTheme(GridComponent);

View file

@ -3,48 +3,51 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type {Grid} from "./GridComponent";
import type {GridType} from './GridComponent'; import GridComponent from "./GridComponent";
import GridComponent from './GridComponent'; import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type PropsType = { type Props = {
items: Array<GridType>, items: Array<Grid>,
style: ViewStyle, style: ViewStyle
}; }
class Preview extends React.PureComponent<PropsType> { class Preview extends React.PureComponent<Props> {
getGrids(): React.Node {
const {items} = this.props;
const grids = [];
items.forEach((item: GridType, index: number) => {
grids.push(Preview.getGridRender(item, index));
});
return grids;
}
static getGridRender(item: GridType, index: number): React.Node { getGrids() {
return ( let grids = [];
<GridComponent for (let i = 0; i < this.props.items.length; i++) {
width={item[0].length} grids.push(this.getGridRender(this.props.items[i], i));
height={item.length} }
grid={item} return grids;
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>;
} }
return null;
} 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;
}
} }
export default withTheme(Preview); export default withTheme(Preview);

View file

@ -1,318 +1,243 @@
// @flow // @flow
import Piece from './Piece'; import Piece from "./Piece";
import ScoreManager from './ScoreManager'; import ScoreManager from "./ScoreManager";
import GridManager from './GridManager'; import GridManager from "./GridManager";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} 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 { export default class GameLogic {
static levelTicks = [1000, 800, 600, 400, 300, 200, 150, 100];
scoreManager: ScoreManager; static levelTicks = [
1000,
800,
600,
400,
300,
200,
150,
100,
];
gridManager: GridManager; #scoreManager: ScoreManager;
#gridManager: GridManager;
height: number; #height: number;
#width: number;
width: number; #gameRunning: boolean;
#gamePaused: boolean;
#gameTime: number;
gameRunning: boolean; #currentObject: Piece;
gamePaused: boolean; #gameTick: number;
#gameTickInterval: IntervalID;
#gameTimeInterval: IntervalID;
gameTime: number; #pressInInterval: TimeoutID;
#isPressedIn: boolean;
#autoRepeatActivationDelay: number;
#autoRepeatDelay: number;
currentObject: Piece; #nextPieces: Array<Piece>;
#nextPiecesCount: number;
gameTick: number; #onTick: Function;
#onClock: Function;
endCallback: Function;
gameTickInterval: IntervalID; #theme: CustomTheme;
gameTimeInterval: 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);
}
pressInInterval: TimeoutID; getHeight(): number {
return this.#height;
}
isPressedIn: boolean; getWidth(): number {
return this.#width;
}
autoRepeatActivationDelay: number; getCurrentGrid() {
return this.#gridManager.getCurrentGrid();
}
autoRepeatDelay: number; isGameRunning(): boolean {
return this.#gameRunning;
}
nextPieces: Array<Piece>; isGamePaused(): boolean {
return this.#gamePaused;
}
nextPiecesCount: number; onFreeze() {
this.#gridManager.freezeTetromino(this.#currentObject, this.#scoreManager);
this.createTetromino();
}
tickCallback: TickCallbackType; setNewGameTick(level: number) {
if (level >= GameLogic.levelTicks.length)
return;
this.#gameTick = GameLogic.levelTicks[level];
clearInterval(this.#gameTickInterval);
this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
}
clockCallback: ClockCallbackType; onTick(callback: Function) {
this.#currentObject.tryMove(0, 1,
endCallback: EndCallbackType; this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight(),
() => this.onFreeze());
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( callback(
this.gridManager.getCurrentGrid(), this.#scoreManager.getScore(),
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
); );
} else callback(this.gridManager.getCurrentGrid());
} }
this.pressInInterval = setTimeout(
() => {
this.movePressedRepeat(false, callback, x, y);
},
isInitial ? this.autoRepeatActivationDelay : this.autoRepeatDelay,
);
}
pressedOut() { pressedOut() {
this.isPressedIn = false; this.#isPressedIn = false;
clearTimeout(this.pressInInterval); 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;
}
recoverNextPiece() { rotatePressed(callback: Function) {
this.currentObject = this.nextPieces.shift(); if (!this.canUseInput())
this.generateNextPieces(); return;
}
generateNextPieces() { if (this.#currentObject.tryRotate(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
while (this.nextPieces.length < this.nextPiecesCount) { callback(this.#gridManager.getCurrentGrid());
this.nextPieces.push(new Piece(this.theme));
} }
}
createTetromino() { getNextPiecesPreviews() {
this.pressedOut(); let finalArray = [];
this.recoverNextPiece(); for (let i = 0; i < this.#nextPieces.length; i++) {
if ( const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length;
!this.currentObject.isPositionValid( finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize));
this.gridManager.getCurrentGrid(), this.#nextPieces[i].toGrid(finalArray[i], true);
this.getWidth(), }
this.getHeight(),
)
)
this.endGame(false);
}
togglePause() { return finalArray;
if (!this.gameRunning) return; }
this.gamePaused = !this.gamePaused;
if (this.gamePaused) this.stopGameTime();
else this.startGameTime();
}
endGame(isRestart: boolean) { recoverNextPiece() {
this.gameRunning = false; this.#currentObject = this.#nextPieces.shift();
this.gamePaused = false; this.generateNextPieces();
this.stopGameTime(); }
this.endCallback(this.gameTime, this.scoreManager.getScore(), isRestart);
}
startGame( generateNextPieces() {
tickCallback: TickCallbackType, while (this.#nextPieces.length < this.#nextPiecesCount) {
clockCallback: ClockCallbackType, this.#nextPieces.push(new Piece(this.#theme));
endCallback: EndCallbackType, }
) { }
if (this.gameRunning) this.endGame(true);
this.gameRunning = true; createTetromino() {
this.gamePaused = false; this.pressedOut();
this.gameTime = 0; this.recoverNextPiece();
this.scoreManager = new ScoreManager(); if (!this.#currentObject.isPositionValid(this.#gridManager.getCurrentGrid(), this.getWidth(), this.getHeight()))
this.gameTick = GameLogic.levelTicks[this.scoreManager.getLevel()]; this.endGame(false);
this.gridManager = new GridManager( }
this.getWidth(),
this.getHeight(), togglePause() {
this.theme, if (!this.#gameRunning)
); return;
this.nextPieces = []; this.#gamePaused = !this.#gamePaused;
this.generateNextPieces(); if (this.#gamePaused) {
this.createTetromino(); clearInterval(this.#gameTickInterval);
tickCallback( clearInterval(this.#gameTimeInterval);
this.scoreManager.getScore(), } else {
this.scoreManager.getLevel(), this.#gameTickInterval = setInterval(this.#onTick, this.#gameTick);
this.gridManager.getCurrentGrid(), this.#gameTimeInterval = setInterval(this.#onClock, 1000);
); }
clockCallback(this.gameTime); }
this.startTick();
this.startClock(); stopGame() {
this.tickCallback = tickCallback; this.#gameRunning = false;
this.clockCallback = clockCallback; this.#gamePaused = false;
this.endCallback = endCallback; 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;
}
} }

View file

@ -1,122 +1,120 @@
// @flow // @flow
import Piece from './Piece'; import Piece from "./Piece";
import ScoreManager from './ScoreManager'; import ScoreManager from "./ScoreManager";
import type {CoordinatesType} from '../Shapes/BaseShape'; import type {Coordinates} from '../Shapes/BaseShape';
import type {GridType} from '../components/GridComponent'; import type {Grid} from "../components/GridComponent";
import type {CellType} from '../components/CellComponent'; import type {Cell} from "../components/CellComponent";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
/** /**
* Class used to manage the game grid * Class used to manage the game grid
*/ */
export default class GridManager { export default class GridManager {
#currentGrid: GridType;
#theme: CustomThemeType; #currentGrid: Grid;
#theme: CustomTheme;
/** /**
* Initializes a grid of the given size * Initializes a grid of the given size
* *
* @param width The grid width * @param width The grid width
* @param height The grid height * @param height The grid height
* @param theme Object containing current theme * @param theme Object containing current theme
*/ */
constructor(width: number, height: number, theme: CustomThemeType) { constructor(width: number, height: number, theme: CustomTheme) {
this.#theme = theme; this.#theme = theme;
this.#currentGrid = this.getEmptyGrid(height, width); 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;
}
/** /**
* Gets a new empty grid * Get the current grid
* *
* @param width The grid width * @return {Grid} The current grid
* @param height The grid height */
* @return {GridType} A new empty grid getCurrentGrid(): Grid {
*/ return this.#currentGrid;
getEmptyGrid(height: number, width: number): GridType {
const grid = [];
for (let row = 0; row < height; row += 1) {
grid.push(this.getEmptyLine(width));
} }
return grid;
}
/** /**
* Removes the given lines from the grid, * Get a new empty grid line of the given size
* shifts down every line on top and adds new empty lines on top. *
* * @param width The line size
* @param lines An array of line numbers to remove * @return {Array<Cell>}
* @param scoreManager A reference to the score manager */
*/ getEmptyLine(width: number): Array<Cell> {
clearLines(lines: Array<number>, scoreManager: ScoreManager) { let line = [];
lines.sort(); for (let col = 0; col < width; col++) {
for (let i = 0; i < lines.length; i += 1) { line.push({
this.#currentGrid.splice(lines[i], 1); color: this.#theme.colors.tetrisBackground,
this.#currentGrid.unshift(this.getEmptyLine(this.#currentGrid[0].length)); isEmpty: true,
} key: col.toString(),
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;
}
/** /**
* Freezes the given piece to the grid * Gets a new empty grid
* *
* @param currentObject The piece to freeze * @param width The grid width
* @param scoreManager A reference to the score manager * @param height The grid height
*/ * @return {Grid} A new empty grid
freezeTetromino(currentObject: Piece, scoreManager: ScoreManager) { */
this.clearLines( getEmptyGrid(height: number, width: number): Grid {
this.getLinesToClear(currentObject.getCoordinates()), let grid = [];
scoreManager, 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);
}
} }

View file

@ -1,16 +1,14 @@
// @flow import ShapeL from "../Shapes/ShapeL";
import ShapeI from "../Shapes/ShapeI";
import ShapeL from '../Shapes/ShapeL'; import ShapeJ from "../Shapes/ShapeJ";
import ShapeI from '../Shapes/ShapeI'; import ShapeO from "../Shapes/ShapeO";
import ShapeJ from '../Shapes/ShapeJ'; import ShapeS from "../Shapes/ShapeS";
import ShapeO from '../Shapes/ShapeO'; import ShapeT from "../Shapes/ShapeT";
import ShapeS from '../Shapes/ShapeS'; import ShapeZ from "../Shapes/ShapeZ";
import ShapeT from '../Shapes/ShapeT'; import type {Coordinates} from '../Shapes/BaseShape';
import ShapeZ from '../Shapes/ShapeZ'; import BaseShape from "../Shapes/BaseShape";
import type {CoordinatesType} from '../Shapes/BaseShape'; import type {Grid} from "../components/GridComponent";
import BaseShape from '../Shapes/BaseShape'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {GridType} from '../components/GridComponent';
import type {CustomThemeType} from '../../../managers/ThemeManager';
/** /**
* Class used as an abstraction layer for shapes. * Class used as an abstraction layer for shapes.
@ -18,167 +16,157 @@ import type {CustomThemeType} from '../../../managers/ThemeManager';
* *
*/ */
export default class Piece { export default class Piece {
shapes = [ShapeL, ShapeI, ShapeJ, ShapeO, ShapeS, ShapeT, ShapeZ];
currentShape: BaseShape; #shapes = [
ShapeL,
ShapeI,
ShapeJ,
ShapeO,
ShapeS,
ShapeT,
ShapeZ,
];
#currentShape: BaseShape;
#theme: CustomTheme;
theme: CustomThemeType; /**
* Initializes this piece's color and shape
/** *
* Initializes this piece's color and shape * @param theme Object containing current theme
* */
* @param theme Object containing current theme constructor(theme: CustomTheme) {
*/ this.#currentShape = this.getRandomShape(theme);
constructor(theme: CustomThemeType) { this.#theme = theme;
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;
}
/** /**
* Tries to move the piece by the given offset on the given grid * Gets a random shape object
* *
* @param x Position X offset * @param theme Object containing current theme
* @param y Position Y offset */
* @param grid The grid to move the piece on getRandomShape(theme: CustomTheme) {
* @param width The grid's width return new this.#shapes[Math.floor(Math.random() * 7)](theme);
* @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;
}
/** /**
* Gets this piece used cells coordinates * Removes the piece from the given grid
* *
* @return {Array<CoordinatesType>} An array of coordinates * @param grid The grid to remove the piece from
*/ */
getCoordinates(): Array<CoordinatesType> { removeFromGrid(grid: Grid) {
return this.currentShape.getCellsCoordinates(true); 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
};
}
}
getCurrentShape(): BaseShape { /**
return this.currentShape; * 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;
}
} }

View file

@ -4,100 +4,98 @@
* Class used to manage game score * Class used to manage game score
*/ */
export default class ScoreManager { export default class ScoreManager {
#scoreLinesModifier = [40, 100, 300, 1200];
#score: number; #scoreLinesModifier = [40, 100, 300, 1200];
#level: number; #score: number;
#level: number;
#levelProgression: number;
#levelProgression: number; /**
* Initializes score to 0
/** */
* Initializes score to 0 constructor() {
*/ this.#score = 0;
constructor() { this.#level = 0;
this.#score = 0; this.#levelProgression = 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;
} }
}
/** /**
* Checks if the player can go to the next level. * Gets the current score
* *
* If he can, change the level. * @return {number} The current score
* */
* @return {boolean} True if the current level has changed getScore(): number {
*/ return this.#score;
canLevelUp(): boolean {
const canLevel = this.#levelProgression > this.#level * 5;
if (canLevel) {
this.#levelProgression -= this.#level * 5;
this.#level += 1;
} }
return canLevel;
} /**
* 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;
}
} }

View file

@ -3,447 +3,400 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Caption, IconButton, Text, withTheme} from 'react-native-paper'; import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import i18n from 'i18n-js'; import GameLogic from "../logic/GameLogic";
import {StackNavigationProp} from '@react-navigation/stack'; import type {Grid} from "../components/GridComponent";
import GameLogic from '../logic/GameLogic'; import GridComponent from "../components/GridComponent";
import type {GridType} from '../components/GridComponent'; import Preview from "../components/Preview";
import GridComponent from '../components/GridComponent'; import i18n from "i18n-js";
import Preview from '../components/Preview'; import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
import MaterialHeaderButtons, { import {StackNavigationProp} from "@react-navigation/stack";
Item, import type {CustomTheme} from "../../../managers/ThemeManager";
} from '../../../components/Overrides/CustomHeaderButton'; import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import OptionsDialog from "../../../components/Dialogs/OptionsDialog";
import type {OptionsDialogButtonType} from '../../../components/Dialogs/OptionsDialog';
import OptionsDialog from '../../../components/Dialogs/OptionsDialog';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {highScore: number}}, route: { params: { highScore: number }, ... },
theme: CustomThemeType, theme: CustomTheme,
}; }
type StateType = { type State = {
grid: GridType, grid: Grid,
gameTime: number, gameRunning: boolean,
gameScore: number, gameTime: number,
gameLevel: number, gameScore: number,
gameLevel: number,
dialogVisible: boolean, dialogVisible: boolean,
dialogTitle: string, dialogTitle: string,
dialogMessage: string, dialogMessage: string,
dialogButtons: Array<OptionsDialogButtonType>, dialogButtons: Array<OptionsDialogButton>,
onDialogDismiss: () => void, onDialogDismiss: () => void,
}; }
class GameMainScreen extends React.Component<PropsType, StateType> { class GameMainScreen extends React.Component<Props, State> {
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; logic: GameLogic;
highScore: number | null;
highScore: number | null; 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;
}
constructor(props: PropsType) { componentDidMount() {
super(props); this.props.navigation.setOptions({
this.logic = new GameLogic(20, 10, props.theme); headerRight: this.getRightButton,
this.state = { });
grid: this.logic.getCurrentGrid(), this.startGame();
gameTime: 0, }
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: '',
dialogMessage: '',
dialogButtons: [],
onDialogDismiss: () => {},
};
if (props.route.params != null)
this.highScore = props.route.params.highScore;
}
componentDidMount() { componentWillUnmount() {
const {navigation} = this.props; this.logic.stopGame();
navigation.setOptions({ }
headerRight: this.getRightButton,
});
this.startGame();
}
componentWillUnmount() { getRightButton = () => {
this.logic.endGame(false); return <MaterialHeaderButtons>
} <Item title="pause" iconName="pause" onPress={this.togglePause}/>
</MaterialHeaderButtons>;
}
getRightButton = (): React.Node => { getFormattedTime(seconds: number) {
return ( let date = new Date();
<MaterialHeaderButtons> date.setHours(0);
<Item title="pause" iconName="pause" onPress={this.togglePause} /> date.setMinutes(0);
</MaterialHeaderButtons> 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;
}
onTick = (score: number, level: number, newGrid: GridType) => { onTick = (score: number, level: number, newGrid: Grid) => {
this.setState({ this.setState({
gameScore: score, gameScore: score,
gameLevel: level, gameLevel: level,
grid: newGrid, grid: newGrid,
}); });
}; }
onClock = (time: number) => { onClock = (time: number) => {
this.setState({ this.setState({
gameTime: time, gameTime: time,
}); });
}; }
onDialogDismiss = () => { updateGrid = (newGrid: Grid) => {
this.setState({dialogVisible: false}); this.setState({
}; grid: newGrid,
});
}
onGameEnd = (time: number, score: number, isRestart: boolean) => { updateGridScore = (newGrid: Grid, score: number) => {
const {props, state} = this; this.setState({
this.setState({ grid: newGrid,
gameTime: time, gameScore: score,
gameScore: score, });
}); }
if (!isRestart)
props.navigation.replace('game-start', {
score: state.gameScore,
level: state.gameLevel,
time: state.gameTime,
});
};
getStatusIcons(): React.Node { togglePause = () => {
const {props, state} = this; this.logic.togglePause();
return ( if (this.logic.isGamePaused())
<View this.showPausePopup();
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>
);
}
getScoreIcon(): React.Node { onDialogDismiss = () => this.setState({dialogVisible: false});
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>
);
}
getControlButtons(): React.Node { showPausePopup = () => {
const {props} = this; const onDismiss = () => {
return ( this.togglePause();
<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.onDialogDismiss();
this.startGame(); };
}, this.setState({
}, dialogVisible: true,
{ dialogTitle: i18n.t("screens.game.pause"),
title: i18n.t('screens.game.restart.confirmNo'), dialogMessage: i18n.t("screens.game.pauseMessage"),
onPress: this.showPausePopup, dialogButtons: [
}, {
], title: i18n.t("screens.game.restart.text"),
onDialogDismiss: this.showPausePopup, onPress: this.showRestartConfirm
}); },
}; {
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
startGame = () => { showRestartConfirm = () => {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd); 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,
});
}
render(): React.Node { startGame = () => {
const {props, state} = this; this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
return ( this.setState({
<View style={{flex: 1}}> gameRunning: true,
<View });
style={{ }
flex: 1,
flexDirection: 'row', onGameEnd = (time: number, score: number, isRestart: boolean) => {
}}> this.setState({
{this.getStatusIcons()} gameTime: time,
<View style={{flex: 4}}> gameScore: score,
{this.getScoreIcon()} gameRunning: false,
<GridComponent });
width={this.logic.getWidth()} if (!isRestart)
height={this.logic.getHeight()} this.props.navigation.replace(
grid={state.grid} "game-start",
style={{ {
backgroundColor: props.theme.colors.tetrisBackground, score: this.state.gameScore,
level: this.state.gameLevel,
time: this.state.gameTime,
}
);
}
getStatusIcons() {
return (
<View style={{
flex: 1, flex: 1,
marginLeft: 'auto', marginTop: "auto",
marginRight: 'auto', marginBottom: "auto"
}} }}>
/> <View style={{
</View> 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>
<View style={{flex: 1}}> </View>
<Preview <View style={{
items={this.logic.getNextPiecesPreviews()} marginLeft: 'auto',
style={{ marginRight: 'auto',
marginLeft: 'auto', marginTop: 20,
marginRight: 'auto', }}>
<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={{
marginTop: 10, marginTop: 10,
}} marginBottom: 10,
/> }}>
</View> <View style={{
</View> flexDirection: "row",
{this.getControlButtons()} 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>
);
}
<OptionsDialog
visible={state.dialogVisible}
title={state.dialogTitle}
message={state.dialogMessage}
buttons={state.dialogButtons}
onDismiss={state.onDialogDismiss}
/>
</View>
);
}
} }
export default withTheme(GameMainScreen); export default withTheme(GameMainScreen);

View file

@ -1,440 +1,427 @@
// @flow // @flow
import * as React from 'react'; import * as React from "react";
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from "@react-navigation/stack";
import { import type {CustomTheme} from "../../../managers/ThemeManager";
Button, import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper";
Card, import {View} from "react-native";
Divider, import i18n from "i18n-js";
Headline, import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot";
Paragraph, import MascotPopup from "../../../components/Mascot/MascotPopup";
Text, import AsyncStorageManager from "../../../managers/AsyncStorageManager";
withTheme, import type {Grid} from "../components/GridComponent";
} from 'react-native-paper'; import GridComponent from "../components/GridComponent";
import {View} from 'react-native'; import GridManager from "../logic/GridManager";
import i18n from 'i18n-js'; import Piece from "../logic/Piece";
import * as Animatable from 'react-native-animatable'; import * as Animatable from "react-native-animatable";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from "react-native-linear-gradient";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import SpeechArrow from "../../../components/Mascot/SpeechArrow";
import Mascot, {MASCOT_STYLE} from '../../../components/Mascot/Mascot'; import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
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 GameStatsType = { type GameStats = {
score: number, score: number,
level: number, level: number,
time: number, time: number,
}; }
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params: GameStatsType, params: GameStats
}, },
theme: CustomThemeType, theme: CustomTheme,
}; }
class GameStartScreen extends React.Component<PropsType> { class GameStartScreen extends React.Component<Props> {
gridManager: GridManager;
scores: Array<number>; gridManager: GridManager;
scores: Array<number>;
gameStats: GameStatsType | null; gameStats: GameStats | null;
isHighScore: boolean;
isHighScore: boolean; constructor(props: Props) {
super(props);
constructor(props: PropsType) { this.gridManager = new GridManager(4, 4, props.theme);
super(props); this.scores = AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.gameScores.key);
this.gridManager = new GridManager(4, 4, props.theme); this.scores.sort((a, b) => b - a);
this.scores = AsyncStorageManager.getObject( if (this.props.route.params != null)
AsyncStorageManager.PREFERENCES.gameScores.key, this.recoverGameScore();
);
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>
);
}
getPostGameContent(stats: GameStatsType): React.Node { recoverGameScore() {
const {props} = this; this.gameStats = this.props.route.params;
return ( this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0];
<View for (let i = 0; i < 3; i++) {
style={{ if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
flex: 1, this.scores.splice(i, 0, this.gameStats.score);
}}> break;
<Mascot } else if (this.scores.length <= i) {
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL} this.scores.push(this.gameStats.score);
animated={this.isHighScore} break;
style={{ }
width: this.isHighScore ? '50%' : '30%', }
marginLeft: this.isHighScore ? 'auto' : null, if (this.scores.length > 3)
marginRight: this.isHighScore ? 'auto' : null, this.scores.splice(3, 1);
}} AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.gameScores.key, this.scores);
/>
<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,
}}>
{i18n.t('screens.game.score', {score: stats.score})}
</Text>
<MaterialCommunityIcons
name="star"
color={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={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>
);
}
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 getPiecesBackground() {
style={{ let gridList = [];
marginLeft: place === 2 ? 20 : 'auto', for (let i = 0; i < 18; i++) {
marginRight: place === 3 ? 20 : 'auto', gridList.push(this.gridManager.getEmptyGrid(4, 4));
flexDirection: 'column', const piece = new Piece(this.props.theme);
alignItems: 'center', piece.toGrid(gridList[i], true);
justifyContent: 'flex-end', }
}}> return (
{this.isHighScore && place === 1 ? ( <View style={{
<Animatable.View position: "absolute",
animation="swing" width: "100%",
iterationCount="infinite" height: "100%",
duration={2000}
delay={1000}
useNativeDriver
style={{
position: 'absolute',
top: -20,
}}> }}>
<Animatable.View {gridList.map((item: Grid, index: number) => {
animation="pulse" const size = 10 + Math.floor(Math.random() * 30);
iterationCount="infinite" const top = Math.floor(Math.random() * 100);
useNativeDriver> const rot = Math.floor(Math.random() * 360);
<MaterialCommunityIcons const left = (index % 6) * 20;
name="decagram" const animDelay = size * 20;
color={props.theme.colors.gameGold} const animDuration = 2 * (2000 - (size * 30));
size={150} return (
/> <Animatable.View
</Animatable.View> animation={"fadeInDownBig"}
</Animatable.View> delay={animDelay}
) : null} duration={animDuration}
<MaterialCommunityIcons key={"piece" + index.toString()}
name={icon} style={{
color={this.isHighScore && place === 1 ? '#fff' : color} width: size + "%",
size={size} position: "absolute",
/> top: top + "%",
<Text left: left + "%",
style={{ }}
textAlign: 'center', >
fontWeight: place === 1 ? 'bold' : null, <GridComponent
fontSize, width={4}
}}> height={4}
{score} grid={item}
</Text> style={{
</View> transform: [{rotateZ: rot + "deg"}],
); }}
} />
</Animatable.View>
getTopScoresRender(): React.Node { );
const gold = this.scores.length > 0 ? this.scores[0] : '-'; })}
const silver = this.scores.length > 1 ? this.scores[1] : '-'; </View>
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,
);
}
render(): React.Node { getPostGameContent(stats: GameStats) {
const {props} = this; return (
return ( <View style={{
<View style={{flex: 1}}> flex: 1
{this.getPiecesBackground()} }}>
<LinearGradient <Mascot
style={{flex: 1}} emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
colors={[ animated={this.isHighScore}
`${props.theme.colors.background}00`, style={{
props.theme.colors.background, width: this.isHighScore ? "50%" : "30%",
]} marginLeft: this.isHighScore ? "auto" : null,
start={{x: 0, y: 0}} marginRight: this.isHighScore ? "auto" : null,
end={{x: 0, y: 1}}> }}/>
<CollapsibleScrollView> <SpeechArrow
{this.getMainContent()} style={{marginLeft: this.isHighScore ? "60%" : "20%"}}
<MascotPopup size={20}
prefKey={AsyncStorageManager.PREFERENCES.gameStartShowBanner.key} color={this.props.theme.colors.mascotMessageArrow}
title={i18n.t('screens.game.mascotDialog.title')} />
message={i18n.t('screens.game.mascotDialog.message')} <Card style={{
icon="gamepad-variant" borderColor: this.props.theme.colors.mascotMessageArrow,
buttons={{ borderWidth: 2,
action: null, marginLeft: 20,
cancel: { marginRight: 20,
message: i18n.t('screens.game.mascotDialog.button'), }}>
icon: 'check', <Card.Content>
}, <Headline
}} style={{
emotion={MASCOT_STYLE.COOL} textAlign: "center",
/> color: this.isHighScore
</CollapsibleScrollView> ? this.props.theme.colors.gameGold
</LinearGradient> : this.props.theme.colors.primary
</View> }}>
); {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>
</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>
);
}
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",
}}>
{
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>
: 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() {
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>
);
}
} }
export default withTheme(GameStartScreen); export default withTheme(GameStartScreen);

View file

@ -19,7 +19,7 @@ import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import AnimatedFAB from '../../components/Animations/AnimatedFAB'; import AnimatedFAB from '../../components/Animations/AnimatedFAB';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import LogoutDialog from '../../components/Amicale/LogoutDialog';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
@ -78,7 +78,7 @@ type RawDashboardType = {
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {nextScreen: string, data: {...}}}, route: {params: {nextScreen: string, data: {...}}},
theme: CustomThemeType, theme: CustomTheme,
}; };
type StateType = { type StateType = {

View file

@ -1,139 +1,116 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Button, Card, Paragraph, withTheme} from 'react-native-paper'; import {Avatar, Button, Card, Paragraph, withTheme} from "react-native-paper";
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {Linking} from 'react-native'; import {Linking} from "react-native";
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from "../../managers/ThemeManager";
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
type PropsType = { type Props = {
theme: CustomThemeType, theme: CustomTheme
}; };
const links = { 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 &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 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 Nature du problème :\n\n\n
Étapes pour reproduire ce pb :\n\n\n\n Étapes pour reproduire ce pb :\n\n\n\n
Stp corrige le pb, bien cordialement.`, Stp corrige le pb, bien cordialement.`,
bugsGit: bugsGit: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new',
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new', facebook: "https://www.facebook.com/campus.insat",
facebook: 'https://www.facebook.com/campus.insat', feedbackMail: `mailto:app@amicale-insat.fr?subject=[FEEDBACK] Application CAMPUS
feedbackMail: `mailto:app@amicale-insat.fr?subject=[FEEDBACK] Application CAMPUS
&body=Coucou Arnaud j'ai du feedback\n\n\n\nBien cordialement.`, &body=Coucou Arnaud j'ai du feedback\n\n\n\nBien cordialement.`,
feedbackGit: feedbackGit: "https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new",
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues/new', }
};
class FeedbackScreen extends React.Component<PropsType> { class FeedbackScreen extends React.Component<Props> {
/**
* 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>
);
}
render(): React.Node { /**
const {theme} = this.props; * Gets link buttons
return ( *
<CollapsibleScrollView style={{padding: 5}}> * @param isBug True if buttons should redirect to bug report methods
<Card> * @returns {*}
<Card.Title */
title={i18n.t('screens.feedback.bugs')} getButtons(isBug: boolean) {
subtitle={i18n.t('screens.feedback.bugsSubtitle')} return (
left={({ <Card.Actions style={{
size, flex: 1,
color, flexWrap: 'wrap',
}: { }}>
size: number, <Button
color: number, icon="email"
}): React.Node => ( mode={"contained"}
<Avatar.Icon size={size} color={color} icon="bug" /> style={{
)} marginLeft: 'auto',
/> marginTop: 5,
<Card.Content> }}
<Paragraph>{i18n.t('screens.feedback.bugsDescription')}</Paragraph> onPress={() => Linking.openURL(isBug ? links.bugsMail : links.feedbackMail)}>
<Paragraph style={{color: theme.colors.primary}}> MAIL
{i18n.t('screens.feedback.contactMeans')} </Button>
</Paragraph> <Button
</Card.Content> icon="git"
{FeedbackScreen.getButtons(true)} mode={"contained"}
</Card> 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>
);
}
<Card style={{marginTop: 20, marginBottom: 10}}> render() {
<Card.Title return (
title={i18n.t('screens.feedback.title')} <CollapsibleScrollView style={{padding: 5}}>
subtitle={i18n.t('screens.feedback.feedbackSubtitle')} <Card>
left={({ <Card.Title
size, title={i18n.t('screens.feedback.bugs')}
color, subtitle={i18n.t('screens.feedback.bugsSubtitle')}
}: { left={(props) => <Avatar.Icon {...props} icon="bug"/>}
size: number, />
color: number, <Card.Content>
}): React.Node => ( <Paragraph>
<Avatar.Icon size={size} color={color} icon="comment" /> {i18n.t('screens.feedback.bugsDescription')}
)} </Paragraph>
/> <Paragraph style={{color: this.props.theme.colors.primary}}>
<Card.Content> {i18n.t('screens.feedback.contactMeans')}
<Paragraph> </Paragraph>
{i18n.t('screens.feedback.feedbackDescription')} </Card.Content>
</Paragraph> {this.getButtons(true)}
</Card.Content> </Card>
{FeedbackScreen.getButtons(false)}
</Card> <Card style={{marginTop: 20, marginBottom: 10}}>
</CollapsibleScrollView> <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>
);
}
} }
export default withTheme(FeedbackScreen); export default withTheme(FeedbackScreen);

View file

@ -1,323 +1,267 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import i18n from 'i18n-js'; import type {CustomTheme} from "../../../managers/ThemeManager";
import {Card, List, Switch, ToggleButton, withTheme} from 'react-native-paper';
import {Appearance} from 'react-native-appearance';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import ThemeManager from '../../../managers/ThemeManager'; import ThemeManager from '../../../managers/ThemeManager';
import AsyncStorageManager from '../../../managers/AsyncStorageManager'; import i18n from "i18n-js";
import CustomSlider from '../../../components/Overrides/CustomSlider'; import AsyncStorageManager from "../../../managers/AsyncStorageManager";
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; 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";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; };
type StateType = { type State = {
nightMode: boolean, nightMode: boolean,
nightModeFollowSystem: boolean, nightModeFollowSystem: boolean,
startScreenPickerSelected: string, notificationReminderSelected: number,
isDebugUnlocked: boolean, startScreenPickerSelected: string,
isDebugUnlocked: boolean,
}; };
/** /**
* Class defining the Settings screen. This screen shows controls to modify app preferences. * Class defining the Settings screen. This screen shows controls to modify app preferences.
*/ */
class SettingsScreen extends React.Component<PropsType, StateType> { class SettingsScreen extends React.Component<Props, State> {
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;
this.state = { /**
nightMode: ThemeManager.getNightMode(), * Loads user preferences into state
nightModeFollowSystem: */
AsyncStorageManager.getBool( constructor() {
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key, super();
) && Appearance.getColorScheme() !== 'no-preference', let notifReminder = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.proxiwashNotifications.key);
startScreenPickerSelected: AsyncStorageManager.getString( this.savedNotificationReminder = parseInt(notifReminder);
AsyncStorageManager.PREFERENCES.defaultStartScreen.key, if (isNaN(this.savedNotificationReminder))
), this.savedNotificationReminder = 0;
isDebugUnlocked: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.debugUnlocked.key, 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);
}; };
}
/** /**
* Saves the value for the proxiwash reminder notification time * Saves the value for the proxiwash reminder notification time
* *
* @param value The value to store * @param value The value to store
*/ */
onProxiwashNotifPickerValueChange = (value: number) => { onStartScreenPickerValueChange = (value: string) => {
AsyncStorageManager.set( if (value != null) {
AsyncStorageManager.PREFERENCES.proxiwashNotifications.key, this.setState({startScreenPickerSelected: value});
value, AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.defaultStartScreen.key, value);
); }
}; };
/** /**
* Saves the value for the proxiwash reminder notification time * Returns a picker allowing the user to select the proxiwash reminder notification time
* *
* @param value The value to store * @returns {React.Node}
*/ */
onStartScreenPickerValueChange = (value: string) => { getProxiwashNotifPicker() {
if (value != null) { return (
this.setState({startScreenPickerSelected: value}); <CustomSlider
AsyncStorageManager.set( style={{flex: 1, marginHorizontal: 10, height: 50}}
AsyncStorageManager.PREFERENCES.defaultStartScreen.key, minimumValue={0}
value, maximumValue={10}
); step={1}
} value={this.savedNotificationReminder}
}; onValueChange={this.onProxiwashNotifPickerValueChange}
thumbTintColor={this.props.theme.colors.primary}
/** minimumTrackTintColor={this.props.theme.colors.primary}
* 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});
}
};
/**
* 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} />
)}
/>
);
}
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}
/>
);
}
/**
* 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={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" />
)}
/> />
{this.getStartScreenPicker()} );
{this.getNavigateItem( }
'dashboard-edit',
'view-dashboard', /**
i18n.t('screens.settings.dashboard'), * Returns a picker allowing the user to select the start screen
i18n.t('screens.settings.dashboardSub'), *
)} * @returns {React.Node}
</List.Section> */
</Card> getStartScreenPicker() {
<Card style={{margin: 5}}> return (
<Card.Title title="Proxiwash" /> <ToggleButton.Row
<List.Section> 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>
);
}
/**
* Toggles night mode and saves it to preferences
*/
onToggleNightMode = () => {
ThemeManager.getInstance().setNightMode(!this.state.nightMode);
this.setState({nightMode: !this.state.nightMode});
};
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});
}
};
/**
* 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 (
<List.Item <List.Item
title={i18n.t('screens.settings.proxiwashNotifReminder')} title={title}
description={i18n.t('screens.settings.proxiwashNotifReminderSub')} description={subtitle}
left={({ left={props => <List.Icon {...props} icon={icon}/>}
size, right={() =>
color, <Switch
}: { value={state}
size: number, onValueChange={onPressCallback}
color: number, />}
}): React.Node => (
<List.Icon size={size} color={color} icon="washing-machine" />
)}
/> />
<View style={{marginLeft: 30}}> );
{this.getProxiwashNotifPicker()} }
</View>
</List.Section> getNavigateItem(route: string, icon: string, title: string, subtitle: string, onLongPress?: () => void) {
</Card> return (
<Card style={{margin: 5}}> <List.Item
<Card.Title title={i18n.t('screens.settings.information')} /> title={title}
<List.Section> description={subtitle}
{isDebugUnlocked onPress={() => this.props.navigation.navigate(route)}
? this.getNavigateItem( left={props => <List.Icon {...props} icon={icon}/>}
'debug', right={props => <List.Icon {...props} icon={"chevron-right"}/>}
'bug-check', onLongPress={onLongPress}
i18n.t('screens.debug.title'), />
'', );
) }
: null}
{this.getNavigateItem( render() {
'about', return (
'information', <CollapsibleScrollView>
i18n.t('screens.about.title'), <Card style={{margin: 5}}>
i18n.t('screens.about.buttonDesc'), <Card.Title title={i18n.t('screens.settings.generalCard')}/>
this.unlockDebugMode, <List.Section>
)} {Appearance.getColorScheme() !== 'no-preference' ? this.getToggleItem(
{this.getNavigateItem( this.onToggleNightModeFollowSystem,
'feedback', 'theme-light-dark',
'comment-quote', i18n.t('screens.settings.nightModeAuto'),
i18n.t('screens.feedback.homeButtonTitle'), i18n.t('screens.settings.nightModeAutoSub'),
i18n.t('screens.feedback.homeButtonSubtitle'), this.state.nightModeFollowSystem
)} ) : null}
</List.Section> {
</Card> Appearance.getColorScheme() === 'no-preference' || !this.state.nightModeFollowSystem ?
</CollapsibleScrollView> 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>
);
}
} }
export default withTheme(SettingsScreen); export default withTheme(SettingsScreen);

View file

@ -6,7 +6,7 @@ import i18n from 'i18n-js';
import {View} from 'react-native'; import {View} from 'react-native';
import {CommonActions} from '@react-navigation/native'; import {CommonActions} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
import ThemeManager from '../../managers/ThemeManager'; import ThemeManager from '../../managers/ThemeManager';
import WebViewScreen from '../../components/Screens/WebViewScreen'; import WebViewScreen from '../../components/Screens/WebViewScreen';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
@ -22,7 +22,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {group: PlanexGroupType}}, route: {params: {group: PlanexGroupType}},
theme: CustomThemeType, theme: CustomTheme,
}; };
type StateType = { type StateType = {

View file

@ -2,183 +2,166 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Card, withTheme} from 'react-native-paper';
import ImageModal from 'react-native-image-modal';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning'; import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
import DateManager from '../../managers/DateManager'; import {Card, withTheme} from 'react-native-paper';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import DateManager from "../../managers/DateManager";
import {apiRequest, ERROR_TYPE} from '../../utils/WebData'; import ImageModal from 'react-native-image-modal';
import ErrorView from '../../components/Screens/ErrorView'; import BasicLoadingScreen from "../../components/Screens/BasicLoadingScreen";
import CustomHTML from '../../components/Overrides/CustomHTML'; import {apiRequest, ERROR_TYPE} from "../../utils/WebData";
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import ErrorView from "../../components/Screens/ErrorView";
import type {CustomThemeType} from '../../managers/ThemeManager'; import CustomHTML from "../../components/Overrides/CustomHTML";
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import type {PlanningEventType} from '../../utils/Planning'; import i18n from 'i18n-js';
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {data: PlanningEventType, id: number, eventId: number}}, route: { params: { data: Object, id: number, eventId: number } },
theme: CustomThemeType, theme: CustomTheme
}; };
type StateType = { type State = {
loading: boolean, loading: boolean
}; };
const EVENT_INFO_URL = 'event/info'; const CLUB_INFO_PATH = "event/info";
/** /**
* Class defining a planning event information page. * Class defining a planning event information page.
*/ */
class PlanningDisplayScreen extends React.Component<PropsType, StateType> { class PlanningDisplayScreen extends React.Component<Props, State> {
displayData: null | PlanningEventType;
shouldFetchData: boolean; displayData: Object;
shouldFetchData: boolean;
eventId: number;
errorCode: number;
eventId: number; /**
* Generates data depending on whether the screen was opened from the planning or from a link
*
* @param props
*/
constructor(props) {
super(props);
errorCode: 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();
/** }
* 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();
} }
}
/** /**
* Hides loading and saves fetched data * Fetches data for the current event id from the API
* */
* @param data Received data fetchData = () => {
*/ this.setState({loading: true});
onFetchSuccess = (data: PlanningEventType) => { apiRequest(CLUB_INFO_PATH, 'POST', {id: this.eventId})
this.displayData = data; .then(this.onFetchSuccess)
this.setState({loading: false}); .catch(this.onFetchError);
}; };
/** /**
* Hides loading and saves the error code * Hides loading and saves fetched data
* *
* @param error * @param data Received data
*/ */
onFetchError = (error: number) => { onFetchSuccess = (data: Object) => {
this.errorCode = error; this.displayData = data;
this.setState({loading: false}); this.setState({loading: false});
}; };
/** /**
* Gets content to display * Hides loading and saves the error code
* *
* @returns {*} * @param error
*/ */
getContent(): React.Node { onFetchError = (error: number) => {
const {theme} = this.props; this.errorCode = error;
const {displayData} = this; this.setState({loading: false});
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}
{displayData.description !== null ? ( /**
<Card.Content * Gets content to display
style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> *
<CustomHTML html={displayData.description} /> * @returns {*}
</Card.Content> */
) : ( getContent() {
<View /> let subtitle = getFormattedEventTime(
)} this.displayData["date_begin"], this.displayData["date_end"]);
</CollapsibleScrollView> 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/>}
/** {this.displayData.description !== null ?
* Shows an error view and use a custom message if the event does not exist <Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
* <CustomHTML html={this.displayData.description}/>
* @returns {*} </Card.Content>
*/ : <View/>}
getErrorView(): React.Node { </CollapsibleScrollView>
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}
/>
);
}
/** /**
* Fetches data for the current event id from the API * Shows an error view and use a custom message if the event does not exist
*/ *
fetchData = () => { * @returns {*}
this.setState({loading: true}); */
apiRequest(EVENT_INFO_URL, 'POST', {id: this.eventId}) getErrorView() {
.then(this.onFetchSuccess) if (this.errorCode === ERROR_TYPE.BAD_INPUT)
.catch(this.onFetchError); 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}/>;
}
render(): React.Node { render() {
const {loading} = this.state; if (this.state.loading)
if (loading) return <BasicLoadingScreen />; return <BasicLoadingScreen/>;
if (this.errorCode === 0) return this.getContent(); else if (this.errorCode === 0)
return this.getErrorView(); return this.getContent();
} else
return this.getErrorView();
}
} }
export default withTheme(PlanningDisplayScreen); export default withTheme(PlanningDisplayScreen);

View file

@ -2,282 +2,259 @@
import * as React from 'react'; import * as React from 'react';
import {BackHandler, View} from 'react-native'; import {BackHandler, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {Agenda, LocaleConfig} from 'react-native-calendars'; import {LocaleConfig} from 'react-native-calendars';
import {Avatar, Divider, List} from 'react-native-paper'; import {readData} from "../../utils/WebData";
import {StackNavigationProp} from '@react-navigation/stack'; import type {eventObject} from "../../utils/Planning";
import {readData} from '../../utils/WebData';
import type {PlanningEventType} from '../../utils/Planning';
import { import {
generateEventAgenda, generateEventAgenda,
getCurrentDateString, getCurrentDateString,
getDateOnlyString, getDateOnlyString,
getFormattedEventTime, getFormattedEventTime,
} from '../../utils/Planning'; } from '../../utils/Planning';
import CustomAgenda from '../../components/Overrides/CustomAgenda'; import {Avatar, Divider, List} from 'react-native-paper';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import CustomAgenda from "../../components/Overrides/CustomAgenda";
import MascotPopup from '../../components/Mascot/MascotPopup'; import {StackNavigationProp} from "@react-navigation/stack";
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import MascotPopup from "../../components/Mascot/MascotPopup";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
LocaleConfig.locales.fr = { LocaleConfig.locales['fr'] = {
monthNames: [ monthNames: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
'Janvier', monthNamesShort: ['Janv.', 'Févr.', 'Mars', 'Avril', 'Mai', 'Juin', 'Juil.', 'Août', 'Sept.', 'Oct.', 'Nov.', 'Déc.'],
'Février', dayNames: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'],
'Mars', dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
'Avril', today: 'Aujourd\'hui'
'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 PropsType = {
navigation: StackNavigationProp, type Props = {
navigation: StackNavigationProp,
}
type State = {
refreshing: boolean,
agendaItems: Object,
calendarShowing: boolean,
}; };
type StateType = { const FETCH_URL = "https://www.amicale-insat.fr/api/event/list";
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; const AGENDA_MONTH_SPAN = 3;
/** /**
* Class defining the app's planning screen * Class defining the app's planning screen
*/ */
class PlanningScreen extends React.Component<PropsType, StateType> { class PlanningScreen extends React.Component<Props, State> {
agendaRef: null | Agenda;
lastRefresh: Date; agendaRef: Object;
minTimeBetweenRefresh = 60; lastRefresh: Date;
minTimeBetweenRefresh = 60;
currentDate = getDateOnlyString(getCurrentDateString()); state = {
refreshing: false,
constructor(props: PropsType) { agendaItems: {},
super(props); calendarShowing: false,
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) {
* Overrides default android back button behaviour to close the calendar if it was open. super(props);
* if (i18n.currentLocale().startsWith("fr")) {
* @return {boolean} LocaleConfig.defaultLocale = 'fr';
*/ }
onBackButtonPressAndroid = (): boolean => {
const {calendarShowing} = this.state;
if (calendarShowing && this.agendaRef != null) {
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
return true;
} }
return false;
};
/** /**
* Refreshes data and shows an animation while doing it * Captures focus and blur events to hook on android back button
*/ */
onRefresh = () => { componentDidMount() {
let canRefresh; this.onRefresh();
if (this.lastRefresh !== undefined) this.props.navigation.addListener(
canRefresh = 'focus',
(new Date().getTime() - this.lastRefresh.getTime()) / 1000 > () =>
this.minTimeBetweenRefresh; BackHandler.addEventListener(
else canRefresh = true; 'hardwareBackPress',
this.onBackButtonPressAndroid
if (canRefresh) { )
this.setState({refreshing: true}); );
readData(FETCH_URL) this.props.navigation.addListener(
.then((fetchedData: Array<PlanningEventType>) => { 'blur',
this.setState({ () =>
refreshing: false, BackHandler.removeEventListener(
agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN), 'hardwareBackPress',
}); this.onBackButtonPressAndroid
this.lastRefresh = new Date(); )
}) );
.catch(() => {
this.setState({
refreshing: false,
});
});
} }
};
/** /**
* Callback used when receiving the agenda ref * Overrides default android back button behaviour to close the calendar if it was open.
* *
* @param ref * @return {boolean}
*/ */
onAgendaRef = (ref: Agenda) => { onBackButtonPressAndroid = () => {
this.agendaRef = ref; if (this.state.calendarShowing) {
}; this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
return true;
/** } else {
* Callback used when a button is pressed to toggle the calendar return false;
* }
* @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,
});
}; };
if (item.logo !== null) {
return ( /**
<View> * Function used to check if a row has changed
<Divider /> *
<List.Item * @param r1
title={item.title} * @param r2
description={getFormattedEventTime(item.date_begin, item.date_end)} * @return {boolean}
left={(): React.Node => ( */
<Avatar.Image rowHasChanged(r1: Object, r2: Object) {
source={{uri: item.logo}} return false;
style={{backgroundColor: 'transparent'}} // if (r1 !== undefined && r2 !== undefined)
/> // return r1.title !== r2.title;
)} // else return !(r1 === undefined && r2 === undefined);
onPress={onPress}
/>
</View>
);
} }
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 * Refreshes data and shows an animation while doing it
* */
* @return {*} onRefresh = () => {
*/ let canRefresh;
getRenderEmptyDate = (): React.Node => <Divider />; if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
else
canRefresh = true;
render(): React.Node { if (canRefresh) {
const {state, props} = this; this.setState({refreshing: true});
return ( readData(FETCH_URL)
<View style={{flex: 1}}> .then((fetchedData) => {
<CustomAgenda this.setState({
// eslint-disable-next-line react/jsx-props-no-spreading refreshing: false,
{...props} agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN)
// 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 this.lastRefresh = new Date();
// considered that the date in question is not yet loaded })
items={state.agendaItems} .catch(() => {
// initially selected day this.setState({
selected={this.currentDate} refreshing: false,
// 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. * Callback used when receiving the agenda ref
onRefresh={this.onRefresh} *
// callback that fires when the calendar is opened or closed * @param ref
onCalendarToggled={this.onCalendarToggled} */
// Set this true while waiting for new data from a refresh onAgendaRef = (ref: Object) => {
refreshing={state.refreshing} this.agendaRef = ref;
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} * Callback used when a button is pressed to toggle the calendar
// ref to this agenda in order to handle back button event *
onRef={this.onAgendaRef} * @param isCalendarOpened True is the calendar is already open, false otherwise
/> */
<MascotPopup onCalendarToggled = (isCalendarOpened: boolean) => {
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key} this.setState({calendarShowing: isCalendarOpened});
title={i18n.t('screens.planning.mascotDialog.title')} }
message={i18n.t('screens.planning.mascotDialog.message')}
icon="party-popper" /**
buttons={{ * Gets an event render item
action: null, *
cancel: { * @param item The current event to render
message: i18n.t('screens.planning.mascotDialog.button'), * @return {*}
icon: 'check', */
}, getRenderItem = (item: eventObject) => {
}} const onPress = this.props.navigation.navigate.bind(this, 'planning-information', {data: item});
emotion={MASCOT_STYLE.HAPPY} if (item.logo !== null) {
/> return (
</View> <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>
);
}
} }
export default PlanningScreen; export default PlanningScreen;

View file

@ -2,117 +2,85 @@
import * as React from 'react'; import * as React from 'react';
import {Image, View} from 'react-native'; 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 {Card, List, Paragraph, Text, Title} from 'react-native-paper';
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proxiwash.png'; type Props = {};
const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proxiwash.png";
/** /**
* Class defining the proxiwash about screen. * Class defining the proxiwash about screen.
*/ */
// eslint-disable-next-line react/prefer-stateless-function export default class ProxiwashAboutScreen extends React.Component<Props> {
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>
<Card style={{margin: 5}}> render() {
<Card.Title return (
title={i18n.t('screens.proxiwash.washer')} <CollapsibleScrollView
left={({ style={{padding: 5}}
size, hasTab={true}
color, >
}: { <View style={{
size: number, width: '100%',
color: string, height: 100,
}): React.Node => ( marginTop: 20,
<List.Icon size={size} color={color} icon="washing-machine" /> marginBottom: 20,
)} justifyContent: 'center',
/> alignItems: 'center'
<Card.Content> }}>
<Title>{i18n.t('screens.proxiwash.procedure')}</Title> <Image
<Paragraph>{i18n.t('screens.proxiwash.washerProcedure')}</Paragraph> source={{uri: LOGO}}
<Title>{i18n.t('screens.proxiwash.tips')}</Title> style={{height: '100%', width: '100%', resizeMode: "contain"}}/>
<Paragraph>{i18n.t('screens.proxiwash.washerTips')}</Paragraph> </View>
</Card.Content> <Text>{i18n.t('screens.proxiwash.description')}</Text>
</Card> <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 style={{margin: 5}}>
<Card.Title <Card.Title
title={i18n.t('screens.proxiwash.tariffs')} title={i18n.t('screens.proxiwash.washer')}
left={({ left={props => <List.Icon {...props} icon={'washing-machine'}/>}
size, />
color, <Card.Content>
}: { <Title>{i18n.t('screens.proxiwash.procedure')}</Title>
size: number, <Paragraph>{i18n.t('screens.proxiwash.washerProcedure')}</Paragraph>
color: string, <Title>{i18n.t('screens.proxiwash.tips')}</Title>
}): React.Node => ( <Paragraph>{i18n.t('screens.proxiwash.washerTips')}</Paragraph>
<List.Icon size={size} color={color} icon="circle-multiple" /> </Card.Content>
)} </Card>
/>
<Card.Content> <Card style={{margin: 5}}>
<Paragraph>{i18n.t('screens.proxiwash.washersTariff')}</Paragraph> <Card.Title
<Paragraph>{i18n.t('screens.proxiwash.dryersTariff')}</Paragraph> title={i18n.t('screens.proxiwash.tariffs')}
</Card.Content> left={props => <List.Icon {...props} icon={'circle-multiple'}/>}
</Card> />
<Card <Card.Content>
style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Paragraph>{i18n.t('screens.proxiwash.washersTariff')}</Paragraph>
<Card.Title <Paragraph>{i18n.t('screens.proxiwash.dryersTariff')}</Paragraph>
title={i18n.t('screens.proxiwash.paymentMethods')} </Card.Content>
left={({ </Card>
size, <Card style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
color, <Card.Title
}: { title={i18n.t('screens.proxiwash.paymentMethods')}
size: number, left={props => <List.Icon {...props} icon={'cash'}/>}
color: string, />
}): React.Node => ( <Card.Content>
<List.Icon size={size} color={color} icon="cash" /> <Paragraph>{i18n.t('screens.proxiwash.paymentMethodsDescription')}</Paragraph>
)} </Card.Content>
/> </Card>
<Card.Content> </CollapsibleScrollView>
<Paragraph> );
{i18n.t('screens.proxiwash.paymentMethodsDescription')} }
</Paragraph>
</Card.Content>
</Card>
</CollapsibleScrollView>
);
}
} }

View file

@ -2,480 +2,421 @@
import * as React from 'react'; import * as React from 'react';
import {Alert, View} from 'react-native'; import {Alert, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from "i18n-js";
import WebSectionList from "../../components/Screens/WebSectionList";
import * as Notifications from "../../utils/Notifications";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {Avatar, Button, Card, Text, withTheme} from 'react-native-paper'; import {Avatar, Button, Card, Text, withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import ProxiwashListItem from "../../components/Lists/Proxiwash/ProxiwashListItem";
import {Modalize} from 'react-native-modalize'; import ProxiwashConstants from "../../constants/ProxiwashConstants";
import WebSectionList from '../../components/Screens/WebSectionList'; import CustomModal from "../../components/Overrides/CustomModal";
import * as Notifications from '../../utils/Notifications'; import AprilFoolsManager from "../../managers/AprilFoolsManager";
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import ProxiwashListItem from '../../components/Lists/Proxiwash/ProxiwashListItem'; import ProxiwashSectionHeader from "../../components/Lists/Proxiwash/ProxiwashSectionHeader";
import ProxiwashConstants from '../../constants/ProxiwashConstants'; import type {CustomTheme} from "../../managers/ThemeManager";
import CustomModal from '../../components/Overrides/CustomModal'; import {StackNavigationProp} from "@react-navigation/stack";
import AprilFoolsManager from '../../managers/AprilFoolsManager'; import {getCleanedMachineWatched, getMachineEndDate, isMachineWatched} from "../../utils/Proxiwash";
import MaterialHeaderButtons, { import {Modalize} from "react-native-modalize";
Item, import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
} from '../../components/Overrides/CustomHeaderButton'; import MascotPopup from "../../components/Mascot/MascotPopup";
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 = const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json";
'https://etud.insa-toulouse.fr/~amicale_app/v2/washinsa/washinsa_data.json';
const modalStateStrings = {}; let modalStateStrings = {};
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
export type ProxiwashMachineType = { export type Machine = {
number: string, number: string,
state: string, state: string,
startTime: string, startTime: string,
endTime: string, endTime: string,
donePercent: string, donePercent: string,
remainingTime: string, remainingTime: string,
program: string, program: string,
}
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
}
type State = {
refreshing: boolean,
modalCurrentDisplayItem: React.Node,
machinesWatched: Array<Machine>,
}; };
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 * Class defining the app's proxiwash screen. This screen shows information about washing machines and
* dryers, taken from a scrapper reading proxiwash website * dryers, taken from a scrapper reading proxiwash website
*/ */
class ProxiwashScreen extends React.Component<PropsType, StateType> { class ProxiwashScreen extends React.Component<Props, State> {
/**
* 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: { fetchedData: {
dryers: Array<ProxiwashMachineType>, dryers: Array<Machine>,
washers: Array<ProxiwashMachineType>, washers: Array<Machine>,
};
/**
* 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 = {
* Setup notification channel for android and add listeners to detect notifications fired refreshing: false,
*/ modalCurrentDisplayItem: null,
componentDidMount() { machinesWatched: AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key),
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;
button = { /**
text: isMachineWatched(item, state.machinesWatched) * Creates machine state parameters using current theme and translations
? i18n.t('screens.proxiwash.modal.disableNotifications') */
: i18n.t('screens.proxiwash.modal.enableNotifications'), constructor(props) {
icon: '', super(props);
onPress, modalStateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.modal.ready');
}; modalStateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.modal.running');
message = i18n.t('screens.proxiwash.modal.running', { modalStateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.modal.runningNotStarted');
start: item.startTime, modalStateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.modal.finished');
end: item.endTime, modalStateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.modal.broken');
remaining: remainingTime, modalStateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.modal.error');
program: item.program, modalStateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.modal.unknown');
});
} 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={(): 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>
{button.onPress !== undefined ? ( /**
<Card.Actions> * Setup notification channel for android and add listeners to detect notifications fired
<Button */
icon={button.icon} componentDidMount() {
mode="contained" this.props.navigation.setOptions({
onPress={button.onPress} headerRight: () =>
style={{marginLeft: 'auto', marginRight: 'auto'}}> <MaterialHeaderButtons>
{button.text} <Item title="information" iconName="information" onPress={this.onAboutPress}/>
</Button> </MaterialHeaderButtons>,
</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 * Callback used when pressing the about button.
* * This will open the ProxiwashAboutScreen.
* @param isDryer True if we are only checking for dryer, false for washers */
* @return {number} The number of machines available onAboutPress = () => this.props.navigation.navigate('proxiwash-about');
*/
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 * Extracts the key for the given item
* *
* @param fetchedData * @param item The item to extract the key from
* @return {*} * @return {*} The extracted key
*/ */
createDataset = (fetchedData: { getKeyExtractor = (item: Machine) => item.number;
dryers: Array<ProxiwashMachineType>,
washers: Array<ProxiwashMachineType>, /**
}): SectionListDataType<ProxiwashMachineType> => { * Setups notifications for the machine with the given ID.
const {state} = this; * One notification will be sent at the end of the program.
let data = fetchedData; * Another will be send a few minutes before the end, based on the value of reminderNotifTime
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { *
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy * @param machine The machine to watch
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers); */
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers); 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);
});
}
} }
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 * Shows a warning telling the user notifications are disabled for the app
* */
* @param title The title to use showNotificationsDisabledWarning() {
* @param item The item to display information for in the modal Alert.alert(
* @param isDryer True if the given item is a dryer i18n.t("screens.proxiwash.modal.notificationErrorTitle"),
*/ i18n.t("screens.proxiwash.modal.notificationErrorDescription"),
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 * Adds the given notifications associated to a machine ID to the watchlist, and saves the array to the preferences
* *
* @param machine * @param machine
*/ */
saveNotificationToState(machine: ProxiwashMachineType) { saveNotificationToState(machine: Machine) {
const {machinesWatched} = this.state; let data = this.state.machinesWatched;
const data = machinesWatched; data.push(machine);
data.push(machine); this.saveNewWatchedList(data);
this.saveNewWatchedList(data); }
}
/** /**
* Removes the given index from the watchlist array and saves it to preferences * Removes the given index from the watchlist array and saves it to preferences
* *
* @param selectedMachine * @param machine
*/ */
removeNotificationFromState(selectedMachine: ProxiwashMachineType) { removeNotificationFromState(machine: Machine) {
const {machinesWatched} = this.state; let data = this.state.machinesWatched;
const newList = [...machinesWatched]; for (let i = 0; i < data.length; i++) {
machinesWatched.forEach((machine: ProxiwashMachineType, index: number) => { if (data[i].number === machine.number && data[i].endTime === machine.endTime) {
if ( data.splice(i, 1);
machine.number === selectedMachine.number && break;
machine.endTime === selectedMachine.endTime }
) }
newList.splice(index, 1); this.saveNewWatchedList(data);
}); }
this.saveNewWatchedList(newList);
}
saveNewWatchedList(list: Array<ProxiwashMachineType>) { saveNewWatchedList(list: Array<Machine>) {
this.setState({machinesWatched: list}); this.setState({machinesWatched: list});
AsyncStorageManager.set( AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key, list);
AsyncStorageManager.PREFERENCES.proxiwashWatchedMachines.key, }
list,
);
}
render(): React.Node { /**
const {state} = this; * Creates the dataset to be used by the flatlist
const {navigation} = this.props; *
return ( * @param fetchedData
<View style={{flex: 1}}> * @return {*}
<View */
style={{ createDataset = (fetchedData: Object) => {
position: 'absolute', let data = fetchedData;
width: '100%', if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
height: '100%', data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
}}> AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
<WebSectionList AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
createDataset={this.createDataset} }
navigation={navigation} this.fetchedData = data;
fetchUrl={DATA_URL} this.state.machinesWatched =
renderItem={this.getRenderItem} getCleanedMachineWatched(this.state.machinesWatched, [...data.dryers, ...data.washers]);
renderSectionHeader={this.getRenderSectionHeader} return [
autoRefreshTime={REFRESH_TIME} {
refreshOnFocus title: i18n.t('screens.proxiwash.dryers'),
updateData={state.machinesWatched.length} icon: 'tumble-dryer',
/> data: data.dryers === undefined ? [] : data.dryers,
</View> keyExtractor: this.getKeyExtractor
<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} title: i18n.t('screens.proxiwash.washers'),
/> icon: 'washing-machine',
<CustomModal onRef={this.onModalRef}> data: data.washers === undefined ? [] : data.washers,
{state.modalCurrentDisplayItem} keyExtractor: this.getKeyExtractor
</CustomModal> },
</View> ];
); };
}
/**
* 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}
/>
);
};
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>
);
}
} }
export default withTheme(ProxiwashScreen); export default withTheme(ProxiwashScreen);

View file

@ -19,7 +19,7 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../../components/Overrides/CustomHeaderButton'; } from '../../../components/Overrides/CustomHeaderButton';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import type {ProximoArticleType} from './ProximoMainScreen'; import type {ProximoArticleType} from './ProximoMainScreen';
@ -56,7 +56,7 @@ type PropsType = {
shouldFocusSearchBar: boolean, shouldFocusSearchBar: boolean,
}, },
}, },
theme: CustomThemeType, theme: CustomTheme,
}; };
type StateType = { type StateType = {

View file

@ -8,7 +8,7 @@ import WebSectionList from '../../../components/Screens/WebSectionList';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../../components/Overrides/CustomHeaderButton'; } from '../../../components/Overrides/CustomHeaderButton';
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {SectionListDataType} from '../../../components/Screens/WebSectionList'; import type {SectionListDataType} from '../../../components/Screens/WebSectionList';
const DATA_URL = 'https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json'; const DATA_URL = 'https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json';
@ -43,7 +43,7 @@ export type ProximoDataType = {
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; };
/** /**

View file

@ -2,188 +2,167 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; 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 {Card, Text, withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import AprilFoolsManager from "../../managers/AprilFoolsManager";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
import i18n from 'i18n-js'; 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 = const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json";
'https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; }
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 defining the app's menu screen.
*/ */
class SelfMenuScreen extends React.Component<PropsType> { class SelfMenuScreen extends React.Component<Props> {
/**
* 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();
}
/** /**
* Creates the dataset to be used in the FlatList * Extract a key for the given item
* *
* @param fetchedData * @param item The item to extract the key from
* @return {[]} * @return {*} The extracted key
*/ */
createDataset = ( getKeyExtractor(item: Object) {
fetchedData: Array<RawRuMenuType>, return item !== undefined ? item['name'] : undefined;
): 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;
};
/** /**
* Gets the render section header * Creates the dataset to be used in the FlatList
* *
* @param section The section to render the header from * @param fetchedData
* @return {*} * @return {[]}
*/ */
getRenderSectionHeader = ({ createDataset = (fetchedData: Object) => {
section, let result = [];
}: { if (fetchedData == null || Object.keys(fetchedData).length === 0) {
section: {title: string}, result = [
}): React.Node => { {
return ( title: i18n.t("general.notAvailable"),
<Card data: [],
style={{ keyExtractor: this.getKeyExtractor
width: '95%', }
marginLeft: 'auto', ];
marginRight: 'auto', } else {
marginTop: 5, if (AprilFoolsManager.getInstance().isAprilFoolsEnabled() && fetchedData.length > 0)
marginBottom: 5, fetchedData[0].meal[0].foodcategory = AprilFoolsManager.getFakeMenuItem(fetchedData[0].meal[0].foodcategory);
elevation: 4, // fetched data is an array here
}}> for (let i = 0; i < fetchedData.length; i++) {
<Card.Title result.push(
title={section.title} {
titleStyle={{ title: DateManager.getInstance().getTranslatedDate(fetchedData[i].date),
textAlign: 'center', data: fetchedData[i].meal[0].foodcategory,
}} keyExtractor: this.getKeyExtractor,
subtitleStyle={{ }
textAlign: 'center', );
}} }
style={{ }
paddingLeft: 0, return result
}} };
/>
</Card>
);
};
/** /**
* Gets a FlatList render item * Gets the render section header
* *
* @param item The item to render * @param section The section to render the header from
* @return {*} * @return {*}
*/ */
getRenderItem = ({item}: {item: RuFoodCategoryType}): React.Node => { getRenderSectionHeader = ({section}: Object) => {
const {theme} = this.props; return (
return ( <Card style={{
<Card width: '95%',
style={{ marginLeft: 'auto',
flex: 0, marginRight: 'auto',
marginHorizontal: 10, marginTop: 5,
marginVertical: 5, marginBottom: 5,
}}> elevation: 4,
<Card.Title style={{marginTop: 5}} title={item.name} /> }}>
<View <Card.Title
style={{ title={section.title}
width: '80%', titleStyle={{
marginLeft: 'auto', textAlign: 'center'
marginRight: 'auto', }}
borderBottomWidth: 1, subtitleStyle={{
borderBottomColor: theme.colors.primary, textAlign: 'center'
marginTop: 5, }}
marginBottom: 5, style={{
}} paddingLeft: 0,
/> }}
<Card.Content> />
{item.dishes.map((object: {name: string}): React.Node => </Card>
object.name !== '' ? ( );
<Text };
style={{
marginTop: 5,
marginBottom: 5,
textAlign: 'center',
}}>
{SelfMenuScreen.formatName(object.name)}
</Text>
) : null,
)}
</Card.Content>
</Card>
);
};
/** /**
* Extract a key for the given item * Gets a FlatList render item
* *
* @param item The item to extract the key from * @param item The item to render
* @return {*} The extracted key * @return {*}
*/ */
getKeyExtractor = (item: RuFoodCategoryType): string => item.name; 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>
);
};
render(): React.Node { /**
const {navigation} = this.props; * Formats the given string to make sure it starts with a capital letter
return ( *
<WebSectionList * @param name The string to format
createDataset={this.createDataset} * @return {string} The formatted string
navigation={navigation} */
autoRefreshTime={0} formatName(name: String) {
refreshOnFocus={false} return name.charAt(0) + name.substr(1).toLowerCase();
fetchUrl={DATA_URL} }
renderItem={this.getRenderItem}
renderSectionHeader={this.getRenderSectionHeader} render() {
stickyHeader 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}/>
);
}
} }
export default withTheme(SelfMenuScreen); export default withTheme(SelfMenuScreen);

View file

@ -13,7 +13,7 @@ import {
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import type {CustomThemeType} from '../../managers/ThemeManager'; import type {CustomTheme} from '../../managers/ThemeManager';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
@ -28,7 +28,7 @@ import type {ServiceCategoryType} from '../../managers/ServicesManager';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; };
class ServicesScreen extends React.Component<PropsType> { class ServicesScreen extends React.Component<PropsType> {

View file

@ -6,7 +6,7 @@ import {CommonActions} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import withCollapsible from '../../utils/withCollapsible'; import {withCollapsible} from '../../utils/withCollapsible';
import type {ServiceCategoryType} from '../../managers/ServicesManager'; import type {ServiceCategoryType} from '../../managers/ServicesManager';
type PropsType = { type PropsType = {

View file

@ -1,116 +1,109 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from "@react-navigation/stack";
import WebViewScreen from '../../components/Screens/WebViewScreen'; import WebViewScreen from "../../components/Screens/WebViewScreen";
import AvailableWebsites from '../../constants/AvailableWebsites'; import AvailableWebsites from "../../constants/AvailableWebsites";
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import BasicLoadingScreen from "../../components/Screens/BasicLoadingScreen";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {host: string, path: string | null, title: string}}, route: { params: { host: string, path: string | null, title: string } },
}; }
const ENABLE_MOBILE_STRING = `<meta name="viewport" content="width=device-width, initial-scale=1.0">`; class WebsiteScreen extends React.Component<Props> {
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>`; fullUrl: string;
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>`; injectedJS: { [key: string]: string };
customPaddingFunctions: {[key: string]: (padding: string) => string}
const BIB_BACK_BUTTON = host: string;
`<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>`;
class WebsiteScreen extends React.Component<PropsType> { constructor(props: Props) {
fullUrl: string; 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;';
injectedJS: {[key: string]: 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;';
customPaddingFunctions: {[key: string]: (padding: string) => string}; this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = (padding: string) => {
return (
host: string; "$('head').append('<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">');" +
"$('.minwidth').css('top', " + padding + ");" +
constructor(props: PropsType) { "$('#mailview-bottom').css('min-height', 500);"
super(props); );
props.navigation.addListener('focus', this.onScreenFocus); };
this.injectedJS = {}; this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = (padding: string) => {
this.customPaddingFunctions = {}; return (
this.injectedJS[AvailableWebsites.websites.AVAILABLE_ROOMS] = "$('#p-logo-text').css('top', 10 + " + padding + ");" +
`document.querySelector('head').innerHTML += '${ENABLE_MOBILE_STRING}';` + "$('#site-navigation h2').css('top', 10 + " + padding + ");" +
`document.querySelector('head').innerHTML += '${AVAILABLE_ROOMS_STYLE}'; true;`; "$('#site-tools h2').css('top', 10 + " + padding + ");" +
"$('#user-tools h2').css('top', 10 + " + padding + ");"
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});`
);
};
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
*
*/
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(): React.Node { onScreenFocus = () => {
const {navigation} = this.props; this.handleNavigationParams();
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]; 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;
if (title != null)
this.props.navigation.setOptions({title: 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/>
);
}
if (this.fullUrl != null) {
return (
<WebViewScreen
navigation={navigation}
url={this.fullUrl}
customJS={injectedJavascript}
customPaddingFunction={customPadding}
/>
);
} }
return <BasicLoadingScreen />;
}
} }
export default WebsiteScreen; export default WebsiteScreen;

View file

@ -1,85 +1,70 @@
// @flow // @flow
import * as React from 'react';
const speedOffset = 5; 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 * Class used to detect when to show or hide a component based on scrolling
*/ */
export default class AutoHideHandler { export default class AutoHideHandler {
lastOffset: number;
isHidden: boolean; lastOffset: number;
isHidden: boolean;
listeners: Array<ListenerFunctionType>; listeners: Array<Function>;
constructor(startHidden: boolean) { constructor(startHidden: boolean) {
this.listeners = []; this.listeners = [];
this.isHidden = startHidden; 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;
} }
this.lastOffset = nativeEvent.contentOffset.y;
} /**
* 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;
}
} }

View file

@ -1,9 +1,9 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {useTheme} from 'react-native-paper'; import {useTheme} from "react-native-paper";
import {createCollapsibleStack} from 'react-navigation-collapsible'; import {createCollapsibleStack} from "react-navigation-collapsible";
import StackNavigator, {StackNavigationOptions} from '@react-navigation/stack'; import StackNavigator, {StackNavigationOptions} from "@react-navigation/stack";
/** /**
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll. * Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
@ -22,34 +22,32 @@ import StackNavigator, {StackNavigationOptions} from '@react-navigation/stack';
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export function createScreenCollapsibleStack( export function createScreenCollapsibleStack(
name: string, name: string,
Stack: StackNavigator, Stack: StackNavigator,
// eslint-disable-next-line flowtype/no-weak-types component: React.ComponentType<any>,
component: React.ComponentType<any>, title: string,
title: string, useNativeDriver?: boolean,
useNativeDriver?: boolean, options?: StackNavigationOptions,
options?: StackNavigationOptions, headerColor?: string) {
headerColor?: string, const {colors} = useTheme();
): React.Node { const screenOptions = options != null ? options : {};
const {colors} = useTheme(); return createCollapsibleStack(
const screenOptions = options != null ? options : {}; <Stack.Screen
return createCollapsibleStack( name={name}
<Stack.Screen component={component}
name={name} options={{
component={component} title: title,
options={{ headerStyle: {
title, backgroundColor: headerColor!=null ? headerColor :colors.surface,
headerStyle: { },
backgroundColor: headerColor != null ? headerColor : colors.surface, ...screenOptions,
}, }}
...screenOptions, />,
}} {
/>, collapsedColor: headerColor!=null ? headerColor :colors.surface,
{ useNativeDriver: useNativeDriver != null ? useNativeDriver : true, // native driver does not work with webview
collapsedColor: headerColor != null ? headerColor : colors.surface, }
useNativeDriver: useNativeDriver != null ? useNativeDriver : true, // native driver does not work with webview )
},
);
} }
/** /**
@ -64,12 +62,6 @@ export function createScreenCollapsibleStack(
* @param title * @param title
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export function getWebsiteStack( export function getWebsiteStack(name: string, Stack: any, component: any, title: string) {
name: string, return createScreenCollapsibleStack(name, Stack, component, title, false);
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);
} }

View file

@ -3,7 +3,7 @@
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen'; import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen';
import DateManager from '../managers/DateManager'; import DateManager from '../managers/DateManager';
import type {CustomThemeType} from '../managers/ThemeManager'; import type {CustomTheme} from '../managers/ThemeManager';
import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen'; import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen';
/** /**
@ -161,7 +161,7 @@ export function getValidRange(
*/ */
export function generateMarkedDates( export function generateMarkedDates(
isSelection: boolean, isSelection: boolean,
theme: CustomThemeType, theme: CustomTheme,
range: Array<string>, range: Array<string>,
): MarkedDatesObjectType { ): MarkedDatesObjectType {
const markedDates = {}; const markedDates = {};

Some files were not shown because too many files have changed in this diff Show more