Compare commits
5 Commits
8e23124c6b
...
2be9df6094
Author | SHA1 | Date |
---|---|---|
giavaphi | 2be9df6094 | |
Roman Schenk | 5faf61089a | |
Gian-Andrea Hutter | 78a27499a8 | |
David Guler | 590049b9cf | |
David Guler | ad05e9e95a |
|
@ -4,7 +4,6 @@ import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
|
||||||
|
@ -14,12 +13,15 @@ import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class MainFXMLController implements Initializable {
|
public class MainFXMLController implements Initializable {
|
||||||
/**
|
/**
|
||||||
* Caching the panes
|
* Caching the panes
|
||||||
*/
|
*/
|
||||||
private final Map<String, AnchorPane> panes = new HashMap<>();
|
private final Map<String, AnchorPane> panes = new HashMap<>();
|
||||||
|
private static final Logger LOG = Logger.getLogger(MainFXMLController.class.getName());
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button home_button;
|
private Button home_button;
|
||||||
|
@ -100,7 +102,7 @@ public class MainFXMLController implements Initializable {
|
||||||
loadPane("Home.fxml");
|
loadPane("Home.fxml");
|
||||||
styleChangeButton(home_button);
|
styleChangeButton(home_button);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
LOG.log(Level.SEVERE, "Failed to load FXML-Pane!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ import ch.zhaw.gartenverwaltung.types.Plant;
|
||||||
import ch.zhaw.gartenverwaltung.types.Seasons;
|
import ch.zhaw.gartenverwaltung.types.Seasons;
|
||||||
import javafx.beans.property.ListProperty;
|
import javafx.beans.property.ListProperty;
|
||||||
import javafx.beans.property.SimpleListProperty;
|
import javafx.beans.property.SimpleListProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
@ -29,8 +27,11 @@ import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class PlantsController implements Initializable {
|
public class PlantsController implements Initializable {
|
||||||
|
private static final Logger LOG = Logger.getLogger(PlantsController.class.getName());
|
||||||
private final PlantListModel plantListModel = new PlantListModel();
|
private final PlantListModel plantListModel = new PlantListModel();
|
||||||
private Plant selectedPlant = null;
|
private Plant selectedPlant = null;
|
||||||
private final HardinessZone DEFAULT_HARDINESS_ZONE = HardinessZone.ZONE_8A;
|
private final HardinessZone DEFAULT_HARDINESS_ZONE = HardinessZone.ZONE_8A;
|
||||||
|
@ -98,8 +99,10 @@ public class PlantsController implements Initializable {
|
||||||
lookForSelectedListEntry();
|
lookForSelectedListEntry();
|
||||||
try {
|
try {
|
||||||
viewFilteredListBySearch();
|
viewFilteredListBySearch();
|
||||||
} catch (HardinessZoneNotSetException | IOException e) {
|
} catch (HardinessZoneNotSetException e) {
|
||||||
e.printStackTrace();
|
LOG.log(Level.WARNING, "Hardiness Zone not set!");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Could not retrieve data!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +110,7 @@ public class PlantsController implements Initializable {
|
||||||
* set text of list view to plant name
|
* set text of list view to plant name
|
||||||
*/
|
*/
|
||||||
private void setListCellFactory() {
|
private void setListCellFactory() {
|
||||||
list_plants.setCellFactory(param -> new ListCell<Plant>() {
|
list_plants.setCellFactory(param -> new ListCell<>() {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Plant plant, boolean empty) {
|
protected void updateItem(Plant plant, boolean empty) {
|
||||||
super.updateItem(plant, empty);
|
super.updateItem(plant, empty);
|
||||||
|
@ -143,13 +146,15 @@ public class PlantsController implements Initializable {
|
||||||
search_plants.textProperty().addListener((observable, oldValue, newValue) -> {
|
search_plants.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if (newValue.isEmpty()) {
|
if (newValue.isEmpty()) {
|
||||||
fillPlantListWithHardinessZone();
|
fillPlantListWithHardinessZone();
|
||||||
}else {
|
} else {
|
||||||
try {
|
try {
|
||||||
List<Plant> filteredPlants = plantListModel.getFilteredPlantListByString(DEFAULT_HARDINESS_ZONE, newValue);
|
List<Plant> filteredPlants = plantListModel.getFilteredPlantListByString(DEFAULT_HARDINESS_ZONE, newValue);
|
||||||
clearListView();
|
clearListView();
|
||||||
plantListProperty.addAll(filteredPlants);
|
plantListProperty.addAll(filteredPlants);
|
||||||
} catch (HardinessZoneNotSetException | IOException e) {
|
} catch (HardinessZoneNotSetException e) {
|
||||||
e.printStackTrace();
|
LOG.log(Level.WARNING, "Hardiness Zone not set!");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Could not retrieve data!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -163,8 +168,10 @@ public class PlantsController implements Initializable {
|
||||||
try {
|
try {
|
||||||
clearListView();
|
clearListView();
|
||||||
plantListProperty.addAll(plantListModel.getPlantList(plantListModel.getCurrentZone()));
|
plantListProperty.addAll(plantListModel.getPlantList(plantListModel.getCurrentZone()));
|
||||||
} catch (HardinessZoneNotSetException | IOException e) {
|
} catch (HardinessZoneNotSetException e) {
|
||||||
e.printStackTrace();
|
LOG.log(Level.WARNING, "Hardiness Zone not set!");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Could not retrieve data!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,12 +189,9 @@ public class PlantsController implements Initializable {
|
||||||
if (zone.equals(DEFAULT_HARDINESS_ZONE)) {
|
if (zone.equals(DEFAULT_HARDINESS_ZONE)) {
|
||||||
radioButton.setSelected(true);
|
radioButton.setSelected(true);
|
||||||
}
|
}
|
||||||
radioButton.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
radioButton.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
@Override
|
plantListModel.setCurrentZone(zone);
|
||||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
fillPlantListWithHardinessZone();
|
||||||
plantListModel.setCurrentZone(zone);
|
|
||||||
fillPlantListWithHardinessZone();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
climate_zones.getChildren().add(radioButton);
|
climate_zones.getChildren().add(radioButton);
|
||||||
}
|
}
|
||||||
|
@ -207,20 +211,19 @@ public class PlantsController implements Initializable {
|
||||||
if (season.equals(Seasons.AllSEASONS)) {
|
if (season.equals(Seasons.AllSEASONS)) {
|
||||||
radioButton.setSelected(true);
|
radioButton.setSelected(true);
|
||||||
}
|
}
|
||||||
radioButton.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
radioButton.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
@Override
|
if (season.equals(Seasons.AllSEASONS)) {
|
||||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
fillPlantListWithHardinessZone();
|
||||||
if (season.equals(Seasons.AllSEASONS)) {
|
} else {
|
||||||
fillPlantListWithHardinessZone();
|
try {
|
||||||
} else {
|
viewFilteredListBySeason(season);
|
||||||
try {
|
} catch (HardinessZoneNotSetException e) {
|
||||||
viewFilteredListBySeason(season);
|
LOG.log(Level.WARNING, "Hardiness Zone not set!");
|
||||||
} catch (HardinessZoneNotSetException | IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
LOG.log(Level.WARNING, "Could not retrieve data!", e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
seasons.getChildren().add(radioButton);
|
seasons.getChildren().add(radioButton);
|
||||||
}
|
}
|
||||||
|
@ -237,27 +240,24 @@ public class PlantsController implements Initializable {
|
||||||
selectSowDay_button.setDisable(true);
|
selectSowDay_button.setDisable(true);
|
||||||
Image img = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
Image img = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
||||||
img_plant.setImage(img);
|
img_plant.setImage(img);
|
||||||
list_plants.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Plant>() {
|
list_plants.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
@Override
|
if(newValue != null) {
|
||||||
public void changed(ObservableValue<? extends Plant> observable, Plant oldValue, Plant newValue) {
|
selectedPlant = newValue;
|
||||||
if(newValue != null) {
|
description_plant.setText(selectedPlant.description());
|
||||||
selectedPlant = newValue;
|
selectSowDay_button.setDisable(false);
|
||||||
description_plant.setText(selectedPlant.description());
|
Image img1;
|
||||||
selectSowDay_button.setDisable(false);
|
if(selectedPlant.image() != null) {
|
||||||
Image img;
|
img1 = selectedPlant.image();
|
||||||
if(selectedPlant.image() != null) {
|
|
||||||
img = selectedPlant.image();
|
|
||||||
} else {
|
|
||||||
img = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
|
||||||
}
|
|
||||||
img_plant.setImage(img);
|
|
||||||
} else {
|
} else {
|
||||||
selectedPlant = null;
|
img1 = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
||||||
description_plant.setText("");
|
|
||||||
selectSowDay_button.setDisable(true);
|
|
||||||
Image img = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
|
||||||
img_plant.setImage(img);
|
|
||||||
}
|
}
|
||||||
|
img_plant.setImage(img1);
|
||||||
|
} else {
|
||||||
|
selectedPlant = null;
|
||||||
|
description_plant.setText("");
|
||||||
|
selectSowDay_button.setDisable(true);
|
||||||
|
Image img1 = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png")));
|
||||||
|
img_plant.setImage(img1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,6 @@ public class JsonGardenPlan implements GardenPlan {
|
||||||
.registerModule(new Jdk8Module())
|
.registerModule(new Jdk8Module())
|
||||||
.writeValue(new File(dataSource.toURI()), cropMap.values());
|
.writeValue(new File(dataSource.toURI()), cropMap.values());
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
// TODO: Log
|
|
||||||
throw new IOException(INVALID_DATASOURCE_MSG, e);
|
throw new IOException(INVALID_DATASOURCE_MSG, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,12 @@ public class GrowthPhaseTypeDeserializer extends StdDeserializer<GrowthPhaseType
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GrowthPhaseType deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
public GrowthPhaseType deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||||
GrowthPhaseType result = null;
|
GrowthPhaseType result;
|
||||||
String token = parser.getText();
|
String token = parser.getText();
|
||||||
try {
|
try {
|
||||||
result = GrowthPhaseType.valueOf(token.toUpperCase());
|
result = GrowthPhaseType.valueOf(token.toUpperCase());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// TODO: Log
|
throw new IOException(String.format("Bad growth phase type \"%s\"\n", token));
|
||||||
System.err.printf("Bad growth phase type \"%s\"\n", token);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,12 @@ public class HardinessZoneDeserializer extends StdDeserializer<HardinessZone> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HardinessZone deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
public HardinessZone deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||||
HardinessZone result = null;
|
HardinessZone result;
|
||||||
String token = parser.getText();
|
String token = parser.getText();
|
||||||
try {
|
try {
|
||||||
result = HardinessZone.valueOf(token.toUpperCase());
|
result = HardinessZone.valueOf(token.toUpperCase());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// TODO: Log
|
throw new IOException(String.format("Unknown Hardiness Zone \"%s\"\n", token), e);
|
||||||
System.err.printf("Unknown Hardiness Zone \"%s\"\n", token);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,7 @@ public class PlantImageDeserializer extends JsonDeserializer<Image> {
|
||||||
try (InputStream is = new FileInputStream(new File(imageUrl.toURI()))) {
|
try (InputStream is = new FileInputStream(new File(imageUrl.toURI()))) {
|
||||||
result = new Image(is);
|
result = new Image(is);
|
||||||
} catch (IllegalArgumentException | URISyntaxException e) {
|
} catch (IllegalArgumentException | URISyntaxException e) {
|
||||||
// TODO: Log
|
throw new IOException(String.format("Cannot find Image \"%s\"\n", imageUrl.getFile()));
|
||||||
e.printStackTrace();
|
|
||||||
System.err.printf("Cannot find Image \"%s\"\n", imageUrl.getFile());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -116,16 +116,17 @@ public class PlantListModel {
|
||||||
if (searchString.length() == 0) {
|
if (searchString.length() == 0) {
|
||||||
return getPlantList(zone);
|
return getPlantList(zone);
|
||||||
} else if (searchString.charAt(0) == '#') {
|
} else if (searchString.charAt(0) == '#') {
|
||||||
try {
|
if (isPositiveIntegral(searchString.substring(1))) {
|
||||||
return getFilteredPlantListById(zone, Long.parseLong(searchString.substring(1)));
|
Long searchId = Long.parseLong(searchString.substring(1));
|
||||||
} catch (NumberFormatException e) {
|
return getFilteredPlantListById(zone, searchId);
|
||||||
|
} else {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String caseInsensitiveSearchString = searchString.toLowerCase();
|
String caseInsensitiveSearchString = searchString.toLowerCase();
|
||||||
return getFilteredPlantList(zone, plant ->
|
return getFilteredPlantList(zone, plant ->
|
||||||
plant.name().toLowerCase().contains(caseInsensitiveSearchString) ||
|
plant.name().toLowerCase().contains(caseInsensitiveSearchString) ||
|
||||||
plant.description().toLowerCase().contains(caseInsensitiveSearchString)
|
plant.description().toLowerCase().contains(caseInsensitiveSearchString)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,15 +170,24 @@ public class PlantListModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param zone selected hardiness zone
|
* @param zone selected hardiness zone
|
||||||
* @param from the earliest date to for the filter
|
* @param from the earliest date to for the filter
|
||||||
* @param to the lastest date for the filter
|
* @param to the lastest date for the filter
|
||||||
* @return List of Plants with selected saison
|
* @return List of Plants with selected saison
|
||||||
* @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
|
* @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
|
||||||
* @throws IOException If the database cannot be accessed
|
* @throws IOException If the database cannot be accessed
|
||||||
*/
|
*/
|
||||||
public List<Plant> getFilteredPlantListBySaisonWithoutGrowthPhase(HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
|
public List<Plant> getFilteredPlantListBySaisonWithoutGrowthPhase(HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
|
||||||
return getFilteredPlantList(zone, plant -> plant.lifecycle().stream().anyMatch(growthPhase -> growthPhase.startDate().compareTo(from) >= 0 && (growthPhase.startDate().compareTo(to) <= 0)));
|
return getFilteredPlantList(zone, plant -> plant.lifecycle().stream().anyMatch(growthPhase -> growthPhase.startDate().compareTo(from) >= 0 && (growthPhase.startDate().compareTo(to) <= 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string can safely be parsed as a positive Integral value (short/int/long)
|
||||||
|
*
|
||||||
|
* @param subject The string to be tested
|
||||||
|
* @return Whether the string contains only digits
|
||||||
|
*/
|
||||||
|
private boolean isPositiveIntegral(String subject) {
|
||||||
|
return subject != null && subject.matches("[0-9]+");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ module ch.zhaw.gartenverwaltung {
|
||||||
requires com.fasterxml.jackson.databind;
|
requires com.fasterxml.jackson.databind;
|
||||||
requires com.fasterxml.jackson.datatype.jsr310;
|
requires com.fasterxml.jackson.datatype.jsr310;
|
||||||
requires com.fasterxml.jackson.datatype.jdk8;
|
requires com.fasterxml.jackson.datatype.jdk8;
|
||||||
|
requires java.logging;
|
||||||
|
|
||||||
opens ch.zhaw.gartenverwaltung to javafx.fxml;
|
opens ch.zhaw.gartenverwaltung to javafx.fxml;
|
||||||
opens ch.zhaw.gartenverwaltung.types to com.fasterxml.jackson.databind;
|
opens ch.zhaw.gartenverwaltung.types to com.fasterxml.jackson.databind;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"startDate" : "2022-05-01",
|
"startDate" : "2022-05-01",
|
||||||
"endDate" : "2022-05-01",
|
"endDate" : "2022-05-01",
|
||||||
"interval" : 0,
|
"interval" : 0,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : 2,
|
"id" : 2,
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
"startDate" : "2022-05-01",
|
"startDate" : "2022-05-01",
|
||||||
"endDate" : "2022-09-01",
|
"endDate" : "2022-09-01",
|
||||||
"interval" : 2,
|
"interval" : 2,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : 3,
|
"id" : 3,
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
"startDate" : "2022-06-01",
|
"startDate" : "2022-06-01",
|
||||||
"endDate" : "2022-08-01",
|
"endDate" : "2022-08-01",
|
||||||
"interval" : 28,
|
"interval" : 28,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : 4,
|
"id" : 4,
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"startDate" : "2022-07-01",
|
"startDate" : "2022-07-01",
|
||||||
"endDate" : "2022-07-01",
|
"endDate" : "2022-07-01",
|
||||||
"interval" : 0,
|
"interval" : 0,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : 5,
|
"id" : 5,
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
"startDate" : "2022-05-01",
|
"startDate" : "2022-05-01",
|
||||||
"endDate" : "2022-09-01",
|
"endDate" : "2022-09-01",
|
||||||
"interval" : 5,
|
"interval" : 5,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id" : 6,
|
"id" : 6,
|
||||||
|
@ -51,6 +51,6 @@
|
||||||
"startDate" : "2022-09-01",
|
"startDate" : "2022-09-01",
|
||||||
"endDate" : "2022-09-01",
|
"endDate" : "2022-09-01",
|
||||||
"interval" : 0,
|
"interval" : 0,
|
||||||
"cropID" : 0
|
"cropId" : 0
|
||||||
}
|
}
|
||||||
]
|
]
|
Loading…
Reference in New Issue