forked from vergnet/application-amicale
Activate proxiwash notifications and see machine info on alert popup
This commit is contained in:
parent
b42ae421f5
commit
8fa2bd792f
8 changed files with 125 additions and 54 deletions
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||||
import WebDataManager from "../utils/WebDataManager";
|
import WebDataManager from "../utils/WebDataManager";
|
||||||
import {Container, Content, Tab, TabHeading, Tabs, Text} from "native-base";
|
import {Container, Content, Tab, TabHeading, Tabs, Text} from "native-base";
|
||||||
import CustomHeader from "./CustomHeader";
|
import CustomHeader from "./CustomHeader";
|
||||||
import {RefreshControl, SectionList, View} from "react-native";
|
import {RefreshControl, SectionList, View, TouchableHighlight} from "react-native";
|
||||||
import CustomMaterialIcon from "./CustomMaterialIcon";
|
import CustomMaterialIcon from "./CustomMaterialIcon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -15,16 +15,16 @@ type State = {
|
||||||
refreshing: boolean,
|
refreshing: boolean,
|
||||||
firstLoading: boolean,
|
firstLoading: boolean,
|
||||||
fetchedData: Object,
|
fetchedData: Object,
|
||||||
machinesWatched: Array<Object>
|
machinesWatched: Array<Object>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FetchedDataSectionList extends React.Component<Props, State> {
|
export default class FetchedDataSectionList extends React.Component<Props, State> {
|
||||||
|
|
||||||
webDataManager: WebDataManager;
|
webDataManager: WebDataManager;
|
||||||
|
|
||||||
constructor() {
|
constructor(fetchUrl: string) {
|
||||||
super();
|
super();
|
||||||
this.webDataManager = new WebDataManager(this.getFetchUrl());
|
this.webDataManager = new WebDataManager(fetchUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -34,10 +34,6 @@ export default class FetchedDataSectionList extends React.Component<Props, State
|
||||||
machinesWatched: [],
|
machinesWatched: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
getFetchUrl() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getHeaderTranslation() {
|
getHeaderTranslation() {
|
||||||
return "Header";
|
return "Header";
|
||||||
}
|
}
|
||||||
|
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -5935,6 +5935,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-native-animatable": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-rmah3KQ63ft8DxkzFUwJSuZeq+oSYwldoGF4DTOR5WM2WR5wiWLgBAtrAHlI3Di3by323uOR21s+MlqPcHz2Kw==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-native-autolink": {
|
"react-native-autolink": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-autolink/-/react-native-autolink-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-autolink/-/react-native-autolink-1.8.1.tgz",
|
||||||
|
@ -6034,6 +6042,15 @@
|
||||||
"prop-types": "^15.6.0"
|
"prop-types": "^15.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-native-modal": {
|
||||||
|
"version": "11.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-11.3.0.tgz",
|
||||||
|
"integrity": "sha512-574hg0dF/gKY0jICg+D4j10F4fKQR8/u88DcVx82LU9QkuYokHr5Rn4E+BoaOUNf3BdNi1z9vzItMQEZa3M8rQ==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react-native-animatable": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-native-platform-touchable": {
|
"react-native-platform-touchable": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-platform-touchable/-/react-native-platform-touchable-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-platform-touchable/-/react-native-platform-touchable-1.1.1.tgz",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"react-native-hyperlink": "0.0.14",
|
"react-native-hyperlink": "0.0.14",
|
||||||
"react-native-image-zoom-viewer": "^2.2.26",
|
"react-native-image-zoom-viewer": "^2.2.26",
|
||||||
"react-native-lightbox": "^0.8.0",
|
"react-native-lightbox": "^0.8.0",
|
||||||
|
"react-native-modal": "^11.3.0",
|
||||||
"react-native-platform-touchable": "^1.1.1",
|
"react-native-platform-touchable": "^1.1.1",
|
||||||
"react-native-scalable-image": "^0.5.1",
|
"react-native-scalable-image": "^0.5.1",
|
||||||
"react-native-status-bar-height": "^2.3.1",
|
"react-native-status-bar-height": "^2.3.1",
|
||||||
|
|
|
@ -26,6 +26,10 @@ function openWebLink(link) {
|
||||||
*/
|
*/
|
||||||
export default class HomeScreen extends FetchedDataSectionList {
|
export default class HomeScreen extends FetchedDataSectionList {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(DATA_URL);
|
||||||
|
}
|
||||||
|
|
||||||
getHeaderTranslation() {
|
getHeaderTranslation() {
|
||||||
return i18n.t("screens.home");
|
return i18n.t("screens.home");
|
||||||
}
|
}
|
||||||
|
@ -51,10 +55,6 @@ export default class HomeScreen extends FetchedDataSectionList {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getFetchUrl() {
|
|
||||||
return DATA_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a dateString using Unix Timestamp to a formatted date
|
* Converts a dateString using Unix Timestamp to a formatted date
|
||||||
* @param dateString {string} The Unix Timestamp representation of a date
|
* @param dateString {string} The Unix Timestamp representation of a date
|
||||||
|
|
|
@ -23,8 +23,8 @@ const typesIcons = {
|
||||||
*/
|
*/
|
||||||
export default class ProximoMainScreen extends FetchedDataSectionList {
|
export default class ProximoMainScreen extends FetchedDataSectionList {
|
||||||
|
|
||||||
getFetchUrl() {
|
constructor() {
|
||||||
return DATA_URL;
|
super(DATA_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderTranslation() {
|
getHeaderTranslation() {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {AsyncStorage, View} from 'react-native';
|
import {Alert, AsyncStorage, View} from 'react-native';
|
||||||
import {Body, Card, CardItem, H2, Left, Right, Text} from 'native-base';
|
import {Body, Card, CardItem, Left, Right, Text} from 'native-base';
|
||||||
import ThemeManager from '../utils/ThemeManager';
|
import ThemeManager from '../utils/ThemeManager';
|
||||||
import i18n from "i18n-js";
|
import i18n from "i18n-js";
|
||||||
import CustomMaterialIcon from "../components/CustomMaterialIcon";
|
import CustomMaterialIcon from "../components/CustomMaterialIcon";
|
||||||
import FetchedDataSectionList from "../components/FetchedDataSectionList";
|
import FetchedDataSectionList from "../components/FetchedDataSectionList";
|
||||||
import NotificationsManager from "../utils/NotificationsManager";
|
import NotificationsManager from "../utils/NotificationsManager";
|
||||||
|
import PlatformTouchable from "react-native-platform-touchable";
|
||||||
|
|
||||||
|
|
||||||
const DATA_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/dataProxiwash.json";
|
const DATA_URL = "https://etud.insa-toulouse.fr/~vergnet/appli-amicale/dataProxiwash.json";
|
||||||
const WATCHED_MACHINES_PREFKEY = "proxiwash.watchedMachines";
|
const WATCHED_MACHINES_PREFKEY = "proxiwash.watchedMachines";
|
||||||
|
@ -23,6 +25,7 @@ const MACHINE_STATES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let stateStrings = {};
|
let stateStrings = {};
|
||||||
|
let modalStateStrings = {};
|
||||||
let stateIcons = {};
|
let stateIcons = {};
|
||||||
let stateColors = {};
|
let stateColors = {};
|
||||||
|
|
||||||
|
@ -44,7 +47,7 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
* Creates machine state parameters using current theme and translations
|
* Creates machine state parameters using current theme and translations
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super(DATA_URL);
|
||||||
let colors = ThemeManager.getInstance().getCurrentThemeVariables();
|
let colors = ThemeManager.getInstance().getCurrentThemeVariables();
|
||||||
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
|
stateColors[MACHINE_STATES.TERMINE] = colors.proxiwashFinishedColor;
|
||||||
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor;
|
stateColors[MACHINE_STATES.DISPONIBLE] = colors.proxiwashReadyColor;
|
||||||
|
@ -58,6 +61,12 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
stateStrings[MACHINE_STATES.HS] = i18n.t('proxiwashScreen.states.broken');
|
stateStrings[MACHINE_STATES.HS] = i18n.t('proxiwashScreen.states.broken');
|
||||||
stateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.states.error');
|
stateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.states.error');
|
||||||
|
|
||||||
|
modalStateStrings[MACHINE_STATES.TERMINE] = i18n.t('proxiwashScreen.modal.finished');
|
||||||
|
modalStateStrings[MACHINE_STATES.DISPONIBLE] = i18n.t('proxiwashScreen.modal.ready');
|
||||||
|
modalStateStrings[MACHINE_STATES.FONCTIONNE] = i18n.t('proxiwashScreen.modal.running');
|
||||||
|
modalStateStrings[MACHINE_STATES.HS] = i18n.t('proxiwashScreen.modal.broken');
|
||||||
|
modalStateStrings[MACHINE_STATES.ERREUR] = i18n.t('proxiwashScreen.modal.error');
|
||||||
|
|
||||||
stateIcons[MACHINE_STATES.TERMINE] = 'check-circle';
|
stateIcons[MACHINE_STATES.TERMINE] = 'check-circle';
|
||||||
stateIcons[MACHINE_STATES.DISPONIBLE] = 'radiobox-blank';
|
stateIcons[MACHINE_STATES.DISPONIBLE] = 'radiobox-blank';
|
||||||
stateIcons[MACHINE_STATES.FONCTIONNE] = 'progress-check';
|
stateIcons[MACHINE_STATES.FONCTIONNE] = 'progress-check';
|
||||||
|
@ -65,10 +74,6 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
stateIcons[MACHINE_STATES.ERREUR] = 'alert';
|
stateIcons[MACHINE_STATES.ERREUR] = 'alert';
|
||||||
}
|
}
|
||||||
|
|
||||||
getFetchUrl() {
|
|
||||||
return DATA_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHeaderTranslation() {
|
getHeaderTranslation() {
|
||||||
return i18n.t("screens.proxiwash");
|
return i18n.t("screens.proxiwash");
|
||||||
}
|
}
|
||||||
|
@ -210,6 +215,35 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAlert(title : string, item : Object, remainingTime: number) {
|
||||||
|
let buttons = [{text: i18n.t("proxiwashScreen.modal.ok")}];
|
||||||
|
let message = modalStateStrings[MACHINE_STATES[item.state]];
|
||||||
|
if (MACHINE_STATES[item.state] === MACHINE_STATES.FONCTIONNE) {
|
||||||
|
buttons = [
|
||||||
|
{
|
||||||
|
text: this.isMachineWatched(item.number) ?
|
||||||
|
i18n.t("proxiwashScreen.modal.disableNotifications") :
|
||||||
|
i18n.t("proxiwashScreen.modal.enableNotifications"),
|
||||||
|
onPress: () => this.setupNotifications(item.number, remainingTime)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n.t("proxiwashScreen.modal.cancel")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
message = i18n.t('proxiwashScreen.modal.running',
|
||||||
|
{
|
||||||
|
start: item.startTime,
|
||||||
|
end: item.endTime,
|
||||||
|
remaining: remainingTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Alert.alert(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
buttons
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list item to be rendered
|
* Get list item to be rendered
|
||||||
*
|
*
|
||||||
|
@ -219,6 +253,12 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
* @returns {React.Node}
|
* @returns {React.Node}
|
||||||
*/
|
*/
|
||||||
getRenderItem(item: Object, section: Object, data: Object) {
|
getRenderItem(item: Object, section: Object, data: Object) {
|
||||||
|
let isMachineRunning = MACHINE_STATES[item.state] === MACHINE_STATES.FONCTIONNE;
|
||||||
|
let machineName = (section.title === i18n.t('proxiwashScreen.dryers') ? i18n.t('proxiwashScreen.dryer') : i18n.t('proxiwashScreen.washer')) + ' n°' + item.number;
|
||||||
|
let remainingTime = 0;
|
||||||
|
if (isMachineRunning)
|
||||||
|
remainingTime = ProxiwashScreen.getRemainingTime(item.startTime, item.endTime, item.donePercent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{
|
<Card style={{
|
||||||
flex: 0,
|
flex: 0,
|
||||||
|
@ -228,7 +268,9 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
<CardItem
|
<CardItem
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: stateColors[MACHINE_STATES[item.state]],
|
backgroundColor: stateColors[MACHINE_STATES[item.state]],
|
||||||
height: '100%'
|
paddingRight: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{
|
<View style={{
|
||||||
|
@ -238,20 +280,38 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
width: item.donePercent !== '' ? (100 - parseInt(item.donePercent)).toString() + '%' : 0,
|
width: item.donePercent !== '' ? (100 - parseInt(item.donePercent)).toString() + '%' : 0,
|
||||||
backgroundColor: ThemeManager.getInstance().getCurrentThemeVariables().containerBgColor
|
backgroundColor: ThemeManager.getInstance().getCurrentThemeVariables().containerBgColor
|
||||||
}}/>
|
}}/>
|
||||||
<Left>
|
<PlatformTouchable
|
||||||
<CustomMaterialIcon icon={section.title === i18n.t('proxiwashScreen.dryers') ? 'tumble-dryer' : 'washing-machine'}
|
onPress={() => this.showAlert(machineName, item, remainingTime)}
|
||||||
|
style={{
|
||||||
|
height: 64,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View/>
|
||||||
|
</PlatformTouchable>
|
||||||
|
<Left style={{marginLeft: 10}}>
|
||||||
|
<CustomMaterialIcon
|
||||||
|
icon={section.title === i18n.t('proxiwashScreen.dryers') ? 'tumble-dryer' : 'washing-machine'}
|
||||||
fontSize={30}
|
fontSize={30}
|
||||||
/>
|
/>
|
||||||
<Body>
|
<Body>
|
||||||
<Text>
|
<Text>
|
||||||
{section.title === i18n.t('proxiwashScreen.dryers') ? i18n.t('proxiwashScreen.dryer') : i18n.t('proxiwashScreen.washer')} n°{item.number}
|
{machineName + ' '}
|
||||||
|
{this.isMachineWatched(item.number) ?
|
||||||
|
<CustomMaterialIcon
|
||||||
|
icon='bell-ring'
|
||||||
|
color={ThemeManager.getInstance().getCurrentThemeVariables().brandPrimary}
|
||||||
|
fontSize={20}
|
||||||
|
/> : ''}
|
||||||
</Text>
|
</Text>
|
||||||
<Text note>
|
<Text note>
|
||||||
{item.startTime !== '' ? item.startTime + '/' + item.endTime : ''}
|
{isMachineRunning ? item.startTime + '/' + item.endTime : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</Body>
|
</Body>
|
||||||
</Left>
|
</Left>
|
||||||
<Right style={{}}>
|
<Right style={{marginRight: 10}}>
|
||||||
<Text style={MACHINE_STATES[item.state] === MACHINE_STATES.TERMINE ?
|
<Text style={MACHINE_STATES[item.state] === MACHINE_STATES.TERMINE ?
|
||||||
{fontWeight: 'bold'} : {}}
|
{fontWeight: 'bold'} : {}}
|
||||||
>
|
>
|
||||||
|
@ -260,33 +320,8 @@ export default class ProxiwashScreen extends FetchedDataSectionList {
|
||||||
<CustomMaterialIcon icon={stateIcons[MACHINE_STATES[item.state]]}
|
<CustomMaterialIcon icon={stateIcons[MACHINE_STATES[item.state]]}
|
||||||
fontSize={25}
|
fontSize={25}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*{item.startTime !== '' ?*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* style={this.isMachineWatched(item.number) ?*/}
|
|
||||||
{/* {backgroundColor: '#ba7c1f'} : {}}*/}
|
|
||||||
{/* onPress={() => {*/}
|
|
||||||
{/* this.setupNotifications(item.number, ProxiwashScreen.getRemainingTime(item.startTime, item.endTime, item.donePercent))*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* <Text>*/}
|
|
||||||
{/* {ProxiwashScreen.getRemainingTime(item.startTime, item.endTime, item.donePercent) + ' ' + i18n.t('proxiwashScreen.min')}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Icon*/}
|
|
||||||
{/* name={this.isMachineWatched(item.number) ? 'bell-ring' : 'bell'}*/}
|
|
||||||
{/* type={'MaterialCommunityIcons'}*/}
|
|
||||||
{/* style={{fontSize: 30, width: 30}}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* : (*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/*}*/}
|
|
||||||
</Right>
|
</Right>
|
||||||
</CardItem>
|
</CardItem>
|
||||||
</Card>);
|
</Card>);
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRenderSectionHeader(title: String) {
|
|
||||||
// return <H2 style={{textAlign: 'center', paddingVertical: 10}}>{title}</H2>;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,17 @@
|
||||||
"listUpdateFail": "Error while updating machines state",
|
"listUpdateFail": "Error while updating machines state",
|
||||||
"error": "Could not update machines state. Pull down to retry.",
|
"error": "Could not update machines state. Pull down to retry.",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
|
"modal": {
|
||||||
|
"enableNotifications": "Notify me",
|
||||||
|
"disableNotifications": "Stop notifications",
|
||||||
|
"ok": "OK",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"finished": "This machine is finished. If you started it, you can get back your laundry.",
|
||||||
|
"ready": "This machine is empty and ready to use. {Put price here}",
|
||||||
|
"running": "This machine has been started at %{start} and will end at %{end}.\nRemaining time: %{remaining} min",
|
||||||
|
"broken": "This machine is broken and cannot be used. Thank you for your comprehension.",
|
||||||
|
"error": "There has been an error and we are unable to get information from this machine. Sorry for the inconvenience."
|
||||||
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"finished": "FINISHED",
|
"finished": "FINISHED",
|
||||||
"ready": "READY",
|
"ready": "READY",
|
||||||
|
|
|
@ -61,6 +61,17 @@
|
||||||
"listUpdateFail": "Erreur lors de la mise à jour del'état des machines",
|
"listUpdateFail": "Erreur lors de la mise à jour del'état des machines",
|
||||||
"error": "Impossible de mettre a jour l'état des machines. Tirez vers le bas pour reessayer.",
|
"error": "Impossible de mettre a jour l'état des machines. Tirez vers le bas pour reessayer.",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
|
"modal": {
|
||||||
|
"enableNotifications": "Me Notifier",
|
||||||
|
"disableNotifications": "Désactiver les notifications",
|
||||||
|
"ok": "OK",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"finished": "Cette machine est terminée. Si vous l'avez l'avez démarée, vous pouvez récupérer votre linge.",
|
||||||
|
"ready": "Cette machine est vide et prête à être utilisée. {Afficher prix ici}",
|
||||||
|
"running": "Cette machine a démarré à %{start} et terminera à %{end}.\nTemps restant : %{remaining} min",
|
||||||
|
"broken": "Cette machine est hors service. Merci pour votre compréhension.",
|
||||||
|
"error": "Il y a eu une erreur et il est impossible de récupérer les informations de cette machine. Veuillez nous excuser pour le gène occasionnée."
|
||||||
|
},
|
||||||
"states": {
|
"states": {
|
||||||
"finished": "TERMINE",
|
"finished": "TERMINE",
|
||||||
"ready": "DISPONIBLE",
|
"ready": "DISPONIBLE",
|
||||||
|
|
Loading…
Reference in a new issue