Merge branch 'develop' into f/groupMessages

This commit is contained in:
delvh 2020-07-08 09:04:51 +02:00 committed by GitHub
commit 8bb6b051b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 231 additions and 64 deletions

34
pom.xml
View File

@ -40,6 +40,18 @@
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>11.0.2</version> <version>11.0.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics </artifactId>
<version>11</version>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics </artifactId>
<version>11</version>
<classifier>linux</classifier>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -61,23 +73,23 @@
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.0</version> <version>3.2.4</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
<goals> <goals>
<goal>single</goal> <goal>shade</goal>
</goals> </goals>
<configuration> <configuration>
<archive> <shadedArtifactAttached>true</shadedArtifactAttached>
<manifest> <sharedClassifierName>envoy</sharedClassifierName>
<mainClass>envoy.client.ui.Startup</mainClass> <transformers>
</manifest> <transformer
</archive> implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<descriptorRefs> <mainClass>envoy.client.Main</mainClass>
<descriptorRef>jar-with-dependencies</descriptorRef> </transformer>
</descriptorRefs> </transformers>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View File

@ -0,0 +1,30 @@
package envoy.client;
import javafx.application.Application;
import envoy.client.ui.Startup;
/**
* Triggers application startup.
* <p>
* To allow Maven shading, the main method has to be separated from the
* {@link Startup} class which extends {@link Application}.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Main.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class Main {
/**
* Starts the application.
*
* @param args the command line arguments are processed by the
* client configuration
* @since Envoy Client v0.1-beta
*/
public static void main(String[] args) { Application.launch(Startup.class, args); }
}

View File

@ -93,6 +93,15 @@ public class Settings {
*/ */
public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); } public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
/**
* @return true if the currently used theme is one of the default themes
* @since Envoy Client v0.1-beta
*/
public boolean isUsingDefaultTheme() {
final var theme = getCurrentTheme();
return theme.equals("dark") || theme.equals("light");
}
/** /**
* @return {@code true}, if pressing the {@code Enter} key suffices to send a * @return {@code true}, if pressing the {@code Enter} key suffices to send a
* message. Otherwise it has to be pressed in conjunction with the * message. Otherwise it has to be pressed in conjunction with the

View File

@ -11,8 +11,6 @@ import javafx.scene.layout.Background;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import envoy.client.data.Settings;
/** /**
* This class offers a text field that is automatically equipped with a clear * This class offers a text field that is automatically equipped with a clear
* button. * button.
@ -49,10 +47,7 @@ public class ClearableTextField extends GridPane {
public ClearableTextField(String text, int size) { public ClearableTextField(String text, int size) {
// initializing the textField and the button // initializing the textField and the button
textField = new TextField(text); textField = new TextField(text);
clearButton = new Button("", clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
new ImageView(IconUtil.load(
Settings.getInstance().getCurrentTheme().equals("dark") ? "/icons/clear_button_white.png" : "/icons/clear_button_black.png",
size)));
clearButton.setOnAction(e -> textField.clear()); clearButton.setOnAction(e -> textField.clear());
clearButton.setFocusTraversable(false); clearButton.setFocusTraversable(false);
clearButton.getStyleClass().clear(); clearButton.getStyleClass().clear();

View File

@ -2,9 +2,13 @@ package envoy.client.ui;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.logging.Level;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import envoy.client.data.Settings;
import envoy.util.EnvoyLog;
/** /**
* Provides static utility methods for loading icons from the resource * Provides static utility methods for loading icons from the resource
* folder. * folder.
@ -21,37 +25,115 @@ public class IconUtil {
private IconUtil() {} private IconUtil() {}
/** /**
* Loads an icon from the resource folder. * Loads an image from the resource folder.
* *
* @param path the path to the icon inside the resource folder * @param path the path to the icon inside the resource folder
* @return the icon * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); } public static Image load(String path) {
Image image = null;
/** try {
* Loads an icon from the resource folder and scales it to a given size. image = new Image(IconUtil.class.getResource(path).toExternalForm());
* } catch (final NullPointerException e) {
* @param path the path to the icon inside the resource folder EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
* @param size the size to scale the icon to }
* @return the scaled icon return image;
* @since Envoy Client v0.1-beta
*/
public static Image load(String path, int size) {
return new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
} }
/** /**
* Loads an image from the resource folder and scales it to the given size.
* *
* Loads icons specified by an enum. The images have to be named like the * @param path the path to the icon inside the resource folder
* @param size the size to scale the icon to
* @return the scaled image
* @since Envoy Client v0.1-beta
*/
public static Image load(String path, int size) {
Image image = null;
try {
image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
} catch (final NullPointerException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
}
/**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* resource folder.<br>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png}.<br>
* To do that, we only have to call {@code IconUtil.loadIcon("abc")}
*/
public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
/**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* resource folder and scales it to the given size.<br>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @param size the size of the image to scale to
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
* To do that, we only have to call
* {@code IconUtil.loadIcon("abc", 16)}
*/
public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
/**
* Loads a {@code .png} image whose design depends on the currently active theme
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* resource folder.
* <p>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the "black" or "white" suffix and without
* the .png suffix
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and
* {@code /icons/light/abc.png}, and load one of them.<br>
* To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc")}
*/
public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
/**
* Loads a {@code .png} image whose design depends on the currently active theme
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* resource folder and scales it to the given size.
* <p>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @param size the size of the image to scale to
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and
* {@code /icons/light/abc.png}, and load one of them in size 16.<br>
* To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc", 16)}
*/
public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
/**
*
* Loads images specified by an enum. The images have to be named like the
* lowercase enum constants with {@code .png} extension and be located inside a * lowercase enum constants with {@code .png} extension and be located inside a
* folder with the lowercase name of the enum, which must be contained inside * folder with the lowercase name of the enum, which must be contained inside
* the {@code /icons} folder. * the {@code /icons/} folder.
* *
* @param <T> the enum that specifies the icons to load * @param <T> the enum that specifies the images to load
* @param enumClass the class of the enum * @param enumClass the class of the enum
* @param size the size to scale the icons to * @param size the size to scale the images to
* @return a map containing the loaded icons with the corresponding enum * @return a map containing the loaded images with the corresponding enum
* constants as keys * constants as keys
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -62,4 +144,17 @@ public class IconUtil {
icons.put(e, load(path + e.toString().toLowerCase() + ".png", size)); icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
return icons; return icons;
} }
/**
* This method should be called if the display of an image depends upon the
* currently active theme.<br>
* In case of a default theme, the string returned will be
* ({@code dark/} or {@code light/}), otherwise it will be empty.
*
* @return the theme specific folder
* @since Envoy Client v0.1-beta
*/
public static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
}
} }

View File

@ -108,7 +108,7 @@ public final class Startup extends Application {
groupMessageStatusCache = new Cache<>(); groupMessageStatusCache = new Cache<>();
stage.setTitle("Envoy"); stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.load("/icons/envoy_logo.png")); stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
final var sceneContext = new SceneContext(stage); final var sceneContext = new SceneContext(stage);
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
@ -135,13 +135,4 @@ public final class Startup extends Application {
logger.log(Level.SEVERE, "Unable to save local files: ", e); logger.log(Level.SEVERE, "Unable to save local files: ", e);
} }
} }
/**
* Starts the application.
*
* @param args the command line arguments are processed by the
* {@link ClientConfig}
* @since Envoy Client v0.1-beta
*/
public static void main(String[] args) { launch(args); }
} }

View File

@ -75,6 +75,9 @@ public final class ChatScene implements Restorable {
@FXML @FXML
private MenuItem deleteContactMenuItem; private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
private LocalDB localDB; private LocalDB localDB;
private Client client; private Client client;
private WriteProxy writeProxy; private WriteProxy writeProxy;
@ -103,7 +106,8 @@ public final class ChatScene implements Restorable {
messageList.setCellFactory(MessageListCellFactory::new); messageList.setCellFactory(MessageListCellFactory::new);
userList.setCellFactory(ContactListCellFactory::new); userList.setCellFactory(ContactListCellFactory::new);
settingsButton.setGraphic(new ImageView(IconUtil.load("/icons/settings.png", 16))); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", 16)));
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20)));
// Listen to received messages // Listen to received messages
eventBus.register(MessageCreationEvent.class, e -> { eventBus.register(MessageCreationEvent.class, e -> {
@ -220,9 +224,9 @@ public final class ChatScene implements Restorable {
if (recorder.isRecording()) { if (recorder.isRecording()) {
recorder.cancel(); recorder.cancel();
recording = false; recording = false;
voiceButton.setText("Record Voice Message");
} }
pendingAttachment = null; pendingAttachment = null;
attachmentView.setVisible(false);
remainingChars.setVisible(true); remainingChars.setVisible(true);
remainingChars remainingChars
@ -260,14 +264,23 @@ public final class ChatScene implements Restorable {
try { try {
if (!recording) { if (!recording) {
recording = true; recording = true;
Platform.runLater(() -> voiceButton.setText("Recording...")); Platform.runLater(() -> {
voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", 24)));
});
recorder.start(); recorder.start();
} else { } else {
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE); pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
recording = false; recording = false;
Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); }); Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 20)));
voiceButton.setText(null);
checkPostConditions(false);
attachmentView.setImage(IconUtil.loadIconThemeSensitive("attachment_present", 20));
attachmentView.setVisible(true);
});
} }
} catch (EnvoyException e) { } catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e); logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait); Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
} }
@ -303,7 +316,7 @@ public final class ChatScene implements Restorable {
private void checkPostConditions(boolean sendKeyPressed) { private void checkPostConditions(boolean sendKeyPressed) {
if (!postingPermanentlyDisabled) { if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && sendKeyPressed) postMessage(); if (!postButton.isDisabled() && sendKeyPressed) postMessage();
postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null); postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
} else { } else {
final var noMoreMessaging = "Go online to send messages"; final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging)) if (!infoLabel.getText().equals(noMoreMessaging))
@ -367,6 +380,7 @@ public final class ChatScene implements Restorable {
if (pendingAttachment != null) { if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment); builder.setAttachment(pendingAttachment);
pendingAttachment = null; pendingAttachment = null;
attachmentView.setVisible(false);
} }
// Building the final message // Building the final message
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient()) final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())

View File

@ -11,7 +11,7 @@
} }
.button:pressed { .button:pressed {
-fx-background-color: darkgray; -fx-background-color: darkviolet;
} }
.button:disabled { .button:disabled {

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.geometry.Rectangle2D?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?> <?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.ContextMenu?> <?import javafx.scene.control.ContextMenu?>
@ -9,6 +10,7 @@
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TextArea?> <?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.Tooltip?> <?import javafx.scene.control.Tooltip?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?> <?import javafx.scene.layout.RowConstraints?>
@ -23,6 +25,9 @@
prefWidth="160.0" /> prefWidth="160.0" />
<ColumnConstraints hgrow="ALWAYS" <ColumnConstraints hgrow="ALWAYS"
maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" /> maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
<ColumnConstraints hgrow="ALWAYS"
maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="7.0"
prefWidth="357.0" />
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>
<RowConstraints maxHeight="-Infinity" <RowConstraints maxHeight="-Infinity"
@ -43,7 +48,7 @@
prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1" prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1"
GridPane.rowSpan="2147483647"> GridPane.rowSpan="2147483647">
<GridPane.margin> <GridPane.margin>
<Insets bottom="10.0" left="10.0" /> <Insets bottom="5.0" left="10.0" />
</GridPane.margin> </GridPane.margin>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
@ -68,8 +73,8 @@
</Label> </Label>
<Button fx:id="settingsButton" mnemonicParsing="true" <Button fx:id="settingsButton" mnemonicParsing="true"
onAction="#settingsButtonClicked" text="_Settings" onAction="#settingsButtonClicked" text="_Settings"
GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.valignment="CENTER"> GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
<GridPane.margin> <GridPane.margin>
<Insets bottom="10.0" right="10.0" top="10.0" /> <Insets bottom="10.0" right="10.0" top="10.0" />
</GridPane.margin> </GridPane.margin>
@ -105,17 +110,21 @@
</contextMenu> </contextMenu>
</ListView> </ListView>
<ButtonBar prefWidth="436.0" GridPane.columnIndex="1" <ButtonBar prefWidth="436.0" GridPane.columnIndex="1"
GridPane.halignment="CENTER" GridPane.rowIndex="5" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
GridPane.valignment="BOTTOM"> GridPane.rowIndex="5" GridPane.valignment="BOTTOM">
<GridPane.margin> <GridPane.margin>
<Insets right="10.0" top="5.0" /> <Insets right="10.0" />
</GridPane.margin> </GridPane.margin>
<buttons> <buttons>
<Button fx:id="voiceButton" disable="true" <Button fx:id="voiceButton" disable="true"
onAction="#voiceButtonClicked" text="_Record Voice Message" /> onAction="#voiceButtonClicked">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
<Button fx:id="postButton" defaultButton="true" <Button fx:id="postButton" defaultButton="true"
disable="true" mnemonicParsing="true" onAction="#postMessage" disable="true" mnemonicParsing="true" onAction="#postMessage"
prefHeight="10.0" prefWidth="75.0" text="_Post"> text="_Post">
<tooltip> <tooltip>
<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true" <Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
maxWidth="350.0" maxWidth="350.0"
@ -140,7 +149,8 @@
onInputMethodTextChanged="#messageTextUpdated" onInputMethodTextChanged="#messageTextUpdated"
onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination" onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
prefHeight="200.0" prefWidth="200.0" wrapText="true" prefHeight="200.0" prefWidth="200.0" wrapText="true"
GridPane.columnIndex="1" GridPane.rowIndex="4"> GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.rowIndex="4">
<GridPane.margin> <GridPane.margin>
<Insets bottom="10.0" left="5.0" right="10.0" top="3.0" /> <Insets bottom="10.0" left="5.0" right="10.0" top="3.0" />
</GridPane.margin> </GridPane.margin>
@ -156,7 +166,7 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
<GridPane.margin> <GridPane.margin>
<Insets bottom="10.0" left="10.0" right="5.0" top="5.0" /> <Insets bottom="10.0" left="10.0" right="5.0" />
</GridPane.margin> </GridPane.margin>
</Button> </Button>
<Label id="remainingCharsLabel" fx:id="remainingChars" <Label id="remainingCharsLabel" fx:id="remainingChars"
@ -189,5 +199,16 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</Label> </Label>
<ImageView fx:id="attachmentView" pickOnBounds="true"
preserveRatio="true" visible="false" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.halignment="RIGHT"
GridPane.rowIndex="3">
<viewport>
<Rectangle2D height="20.0" width="20.0" />
</viewport>
<GridPane.margin>
<Insets bottom="5.0" right="10.0" top="5.0" />
</GridPane.margin>
</ImageView>
</children> </children>
</GridPane> </GridPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB