Compare commits

10 Commits

Author SHA1 Message Date
Alexis Delhaie
82fc930c02 Fix empty response error 2021-01-15 11:57:19 +01:00
Alexis Delhaie
d17133bdbd Rename Bootstrap project folder 2020-12-15 15:07:34 +01:00
Alexis Delhaie
b82ce01f9d Fixing Chuncked Encoding 2020-12-10 11:47:40 +01:00
Alexis Delhaie
bb16995dbb Fixing Basic authentication 2020-12-10 10:37:10 +01:00
Alexis Delhaie
12456e3474 Auth dialog, Parsing URL Parameters 2020-11-18 19:16:58 +01:00
Alexis Delhaie
ebf343b063 Merge branch 'master' of https://github.com/alexlegarnd/EndPoint 2020-10-27 22:02:30 +01:00
Alexis Delhaie
c4910722e9 Adding banner, Response tab, fix method combobox 2020-10-27 22:02:13 +01:00
Alexis
3b1510000d Update README.md 2020-10-25 21:55:15 +01:00
Alexis
d06d8f89f0 Update README.md 2020-10-25 21:54:58 +01:00
Alexis
cf9aff4242 Merge pull request #1 from alexlegarnd/swing
Convert project from JavaFX to Swing
2020-10-25 21:52:35 +01:00
30 changed files with 700 additions and 97 deletions

View File

@@ -5,6 +5,9 @@ Just send a GET, POST, PUT or DELETE HTTP request on a server and get it's respo
# Build
⚠ Nothing is working for now
I use the language level 15 of Java, if you want to use Java 11, you need to refactor some Switches/Cases
# Binaries
Compiled binaries with installer downloadable [here](https://alexisdelhaie.ovh/dlcenter/endpoint-installer.exe). (There is no fancy web page now)
This installer are based on [chronos-installer-bootstrap](https://github.com/alexlegarnd/chronos-installer-bootstrap) and [chronos-installer](https://github.com/alexlegarnd/chronos-installer)

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -6,6 +6,7 @@
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdParam, int iCmdShow)
{
int return_code = 0;
STARTUPINFO si;
PROCESS_INFORMATION pi;
@@ -16,17 +17,15 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdParam
LPTSTR command = _tcsdup(TEXT(".\\runtime\\bin\\javaw.exe -cp .\\app\\EndPoint.jar ovh.alexisdelhaie.endpoint.Application"));
if (!CreateProcess(NULL, command, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
CloseHandle(&pi.hProcess);
CloseHandle(&pi.hThread);
int msgboxID = MessageBox(
NULL,
TEXT("Unable to start the JVM. Try reinstalling the program to fix this problem."),
TEXT("EndPoint bootstrap"),
MB_ICONHAND | MB_OK | MB_DEFBUTTON1
);
return -1;
return_code = -1;
}
CloseHandle(&pi.hProcess);
CloseHandle(&pi.hThread);
return 0;
return return_code;
}

23
pom.xml
View File

@@ -6,12 +6,26 @@
<groupId>ovh.alexisdelhaie</groupId>
<artifactId>EndPoint</artifactId>
<version>0.1.3</version>
<version>0.1.5.3</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>15</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>15</release>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
@@ -31,6 +45,13 @@
<artifactId>flatlaf</artifactId>
<version>0.43</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,17 +1,17 @@
package ovh.alexisdelhaie.endpoint;
import com.formdev.flatlaf.FlatIntelliJLaf;
import ovh.alexisdelhaie.endpoint.configuration.ConfigurationProperties;
import ovh.alexisdelhaie.endpoint.utils.Tools;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
public class Application {
public static void main(String[] args) throws UnsupportedLookAndFeelException, IOException {
UIManager.setLookAndFeel(new FlatIntelliJLaf());
MainWindow dialog = new MainWindow();
public static void main(String[] args) throws UnsupportedLookAndFeelException, IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
ConfigurationProperties props = new ConfigurationProperties();
UIManager.setLookAndFeel(Tools.getLookAndFeel(props.getStringProperty("theme", "IntelliJ")));
MainWindow dialog = new MainWindow(props);
dialog.pack();
dialog.setTitle("EndPoint");
dialog.setVisible(true);

View File

@@ -116,7 +116,7 @@
</hspacer>
<component id="45ca1" class="javax.swing.JButton" binding="settingsButton" default-binding="true">
<constraints>
<grid row="1" column="14" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
<grid row="3" column="15" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Settings"/>

View File

@@ -8,21 +8,26 @@ import ovh.alexisdelhaie.endpoint.http.Request;
import ovh.alexisdelhaie.endpoint.http.RequestBuilder;
import ovh.alexisdelhaie.endpoint.http.Response;
import ovh.alexisdelhaie.endpoint.utils.MessageDialog;
import ovh.alexisdelhaie.endpoint.utils.RequestTab;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MainWindow extends JFrame {
@@ -47,6 +52,7 @@ public class MainWindow extends JFrame {
// Constants
public final static int WIDTH = 1280;
public final static int HEIGHT = 720;
public final static String NEW_TAB_NAME = "New request";
private JPanel contentPane;
private JComboBox<String> methodBox;
@@ -59,41 +65,29 @@ public class MainWindow extends JFrame {
private JButton settingsButton;
private final ConfigurationProperties props;
private final HashMap<Integer, String> urls;
private final ConcurrentHashMap<Integer, RequestTab> tabs;
private final ConcurrentHashMap<Integer, Boolean> controlState;
private final ConcurrentHashMap<Integer, Response> responses;
public MainWindow() throws IOException {
props = new ConfigurationProperties();
controlState = new ConcurrentHashMap<>();
responses = new ConcurrentHashMap<>();
urls = new HashMap<>();
public MainWindow(ConfigurationProperties props) throws IOException {
this.props = props;
tabs = new ConcurrentHashMap<>();
setIconImage(ImageIO.read(MainWindow.class.getResource("/icon.png")));
setContentPane(contentPane);
setMinimumSize(new Dimension(WIDTH, HEIGHT));
setSize(WIDTH, HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TabBuilder.create(tabbedPane1, "New request", urls, urlField);
Component tab = tabbedPane1.getSelectedComponent();
urls.put(tab.hashCode(), "");
enableControl(true, tab.hashCode());
newTab();
settingsButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
ConfigurationDialog.showDialog(props);
ConfigurationDialog.showDialog(props, MainWindow.this);
}
});
newTabButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
TabBuilder.create(tabbedPane1, "New request", urls, urlField);
Component tab = tabbedPane1.getSelectedComponent();
urls.put(tab.hashCode(), "");
enableControl(true, tab.hashCode());
showStatus(tab.hashCode());
newTab();
}
});
sendButton.addMouseListener(new MouseAdapter() {
@@ -110,9 +104,22 @@ public class MainWindow extends JFrame {
tabbedPane1.addChangeListener(e -> {
if (tabbedPane1.getSelectedIndex() != -1) {
int hashCode = tabbedPane1.getSelectedComponent().hashCode();
urlField.setText(urls.get(hashCode));
enableControl(controlState.get(hashCode), hashCode);
showStatus(tab.hashCode());
RequestTab requestTab = tabs.get(hashCode);
if (Objects.nonNull(requestTab)) {
urlField.setText(requestTab.getUrl());
methodBox.setSelectedItem(requestTab.getMethod());
enableControl(requestTab.isRunning(), hashCode);
showStatus(hashCode);
}
}
});
methodBox.addItemListener((e) -> {
if (tabbedPane1.getSelectedIndex() != -1) {
int hashCode = tabbedPane1.getSelectedComponent().hashCode();
RequestTab requestTab = tabs.get(hashCode);
if (Objects.nonNull(requestTab)) {
requestTab.setMethod((String) methodBox.getSelectedItem());
}
}
});
urlField.getDocument().addDocumentListener(new DocumentListener() {
@@ -128,23 +135,59 @@ public class MainWindow extends JFrame {
public void warn() {
if (tabbedPane1.getSelectedIndex() != -1) {
urls.put(tabbedPane1.getSelectedComponent().hashCode(), urlField.getText());
int i = tabbedPane1.indexOfComponent(tabbedPane1.getSelectedComponent());
JLabel title = TabBuilder.getLabel(i);
tabs.get(tabbedPane1.getSelectedComponent().hashCode()).setUrl(urlField.getText());
if (Objects.nonNull(title)) {
if (!urlField.getText().isBlank()) {
try {
URL u = new URL((!urlField.getText().toLowerCase().startsWith("http://") &&
!urlField.getText().toLowerCase().startsWith("https://")) ?
"http://" + urlField.getText() : urlField.getText());
parseParamsFromUrl(urlField.getText());
if (u.getPath().isBlank()) {
title.setText(u.getHost());
} else {
title.setText(String.format("%s (%s)", u.getPath(), u.getHost()));
}
} catch (MalformedURLException e) {
title.setText(urlField.getText());
}
} else {
title.setText(NEW_TAB_NAME);
}
}
}
}
});
}
private void newTab() {
RequestTab requestTab = new RequestTab((String) methodBox.getSelectedItem());
TabBuilder.create(tabbedPane1, NEW_TAB_NAME, tabs, urlField);
Component tab = tabbedPane1.getSelectedComponent();
tabs.put(tab.hashCode(), requestTab);
enableControl(true, tab.hashCode());
showStatus(tab.hashCode());
urlField.setText("");
}
private void sendRequest() {
Optional<JSplitPane> possibleTab = getSelectedTab();
if (possibleTab.isPresent()) {
JSplitPane tab = possibleTab.get();
int tabHashCode = tab.hashCode();
RequestTab requestTab = tabs.get(tabHashCode);
statusLabel.setVisible(false);
enableControl(false, tabHashCode);
int i = tabbedPane1.indexOfComponent(tab);
JTextArea responseBody = TabBuilder.getResponseArea(i);
JTextArea responseHeader = TabBuilder.getResponseHeaderTextArea(i);
JTextArea requestHeader = TabBuilder.getRequestHeaderTextArea(i);
responseBody.setForeground(Color.black);
responseBody.setText("");
responseHeader.setText("");
requestHeader.setText("");
JTextArea bodyField = TabBuilder.getBody(i);
new Thread(() -> {
try {
@@ -163,14 +206,16 @@ public class MainWindow extends JFrame {
}
if (possibleRes.isPresent()) {
Response res = possibleRes.get();
responses.put(tabHashCode, res);
requestTab.setRes(res);
responseBody.setText(res.getBody());
requestHeader.setText(res.getRequest().getRawRequest());
responseHeader.setText(res.getRawHeaders());
}
} catch (KeyManagementException | IOException | NoSuchAlgorithmException e) {
responseBody.setForeground(Color.red);
responseBody.setText(e.getMessage());
if (responses.containsKey(tabHashCode)) {
responses.remove(tabHashCode);
if (Objects.nonNull(requestTab.getRes())) {
requestTab.setRes(null);
showStatus(tabHashCode);
}
} finally {
@@ -185,7 +230,7 @@ public class MainWindow extends JFrame {
private void enableControl(Boolean state, int hashCode) {
if (Objects.nonNull(state)) {
controlState.put(hashCode, state);
tabs.get(hashCode).setRunning(state);
if (tabbedPane1.getSelectedComponent().hashCode() == hashCode) {
sendButton.setEnabled(state);
urlField.setEnabled(state);
@@ -196,10 +241,11 @@ public class MainWindow extends JFrame {
}
private void showStatus(int hashCode) {
if (controlState.get(hashCode) && responses.containsKey(hashCode) &&
RequestTab requestTab = tabs.get(hashCode);
if (requestTab.isRunning() && Objects.nonNull(requestTab.getRes()) &&
tabbedPane1.getSelectedComponent().hashCode() == hashCode) {
statusLabel.setForeground(Color.BLACK);
Response res = responses.get(hashCode);
Response res = requestTab.getRes();
final StringBuilder sb = new StringBuilder();
if (res.getStatusCode() != -1) {
sb.append(res.getStatusCode())
@@ -240,4 +286,25 @@ public class MainWindow extends JFrame {
return Optional.empty();
}
private void parseParamsFromUrl(String url) {
Optional<JSplitPane> possibleTab = getSelectedTab();
if (possibleTab.isPresent()) {
int id = tabbedPane1.indexOfComponent(possibleTab.get());
JTable table = TabBuilder.getParamsTable(id);
DefaultTableModel m = (DefaultTableModel) table.getModel();
for (int i = m.getRowCount() - 1; i > -1; i--) {
m.removeRow(i);
}
Pattern pattern = Pattern.compile("[^&?]*?=[^&?]*", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
while (matcher.find()) {
String param = matcher.group();
String[] kv = param.split("=");
if (kv.length == 2) {
m.addRow(new Object[]{kv[0], kv[1]});
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
package ovh.alexisdelhaie.endpoint.builder;
import javax.swing.table.DefaultTableModel;
public class ReadOnlyTableModel extends DefaultTableModel {
public ReadOnlyTableModel(Object[][] objects, String[] headers) {
super(objects, headers);
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
}

View File

@@ -1,10 +1,8 @@
package ovh.alexisdelhaie.endpoint.builder;
import ovh.alexisdelhaie.endpoint.utils.RequestTab;
import ovh.alexisdelhaie.endpoint.utils.Tools;
import ovh.alexisdelhaie.endpoint.utils.adapter.CustomDeleteMouseAdapter;
import ovh.alexisdelhaie.endpoint.utils.adapter.CustomNewMouseAdapter;
import ovh.alexisdelhaie.endpoint.utils.adapter.DeleteParamMouseAdapter;
import ovh.alexisdelhaie.endpoint.utils.adapter.NewParamMouseAdapter;
import ovh.alexisdelhaie.endpoint.utils.adapter.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
@@ -12,17 +10,18 @@ import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
public class TabBuilder {
private static final HashMap<String, Component> indexes = new HashMap<>();
public static final ConcurrentHashMap<String, Component> indexes = new ConcurrentHashMap<>();
public static void create(JTabbedPane tab, String label, HashMap<Integer, String> urls, JTextField urlField) {
public static void create(JTabbedPane tab, String label, ConcurrentHashMap<Integer, RequestTab> tabs, JTextField urlField) {
Component c = tab.add("", buildMainPanel(urlField));
int index = tab.indexOfComponent(c);
updateIndexes(index);
tab.setTabComponentAt(index, buildTabPanel(tab, c, label, urls));
tab.setTabComponentAt(index, buildTabPanel(tab, c, label, tabs));
tab.setSelectedComponent(c);
updateIndexes(index);
}
private static void updateIndexes(int index) {
@@ -30,16 +29,23 @@ public class TabBuilder {
indexes.put("main[" + index + "].body", indexes.get("main[waiting].body"));
indexes.put("main[" + index + "].params", indexes.get("main[waiting].params"));
indexes.put("main[" + index + "].headers", indexes.get("main[waiting].headers"));
indexes.put("main[" + index + "].tabTitle", indexes.get("main[waiting].tabTitle"));
indexes.put("main[" + index + "].requestHeaderTextArea", indexes.get("main[waiting].requestHeaderTextArea"));
indexes.put("main[" + index + "].responseHeaderTextArea", indexes.get("main[waiting].responseHeaderTextArea"));
indexes.remove("main[waiting].responseTextArea");
indexes.remove("main[waiting].body");
indexes.remove("main[waiting].params");
indexes.remove("main[waiting].headers");
indexes.remove("main[waiting].tabTitle");
indexes.remove("main[waiting].requestHeaderTextArea");
indexes.remove("main[waiting].responseHeaderTextArea");
}
private static JPanel buildTabPanel(JTabbedPane tab, Component c, String label, HashMap<Integer, String> urls) {
private static JPanel buildTabPanel(JTabbedPane tab, Component c, String label, ConcurrentHashMap<Integer, RequestTab> tabs) {
JPanel p = new JPanel(new GridBagLayout());
p.setOpaque(false);
JLabel l = new JLabel(label);
indexes.put("main[waiting].tabTitle", l);
GridBagConstraints g = new GridBagConstraints();
g.gridx = 0;
g.gridy = 0;
@@ -47,11 +53,11 @@ public class TabBuilder {
p.add(l, g);
g.gridx++;
g.weightx = 0;
p.add(buildCloseButton(tab, c, urls), g);
p.add(buildCloseButton(tab, c, tabs), g);
return p;
}
private static JButton buildCloseButton(JTabbedPane tab, Component c, HashMap<Integer, String> urls) {
private static JButton buildCloseButton(JTabbedPane tab, Component c, ConcurrentHashMap<Integer, RequestTab> tabs) {
JButton b = new JButton("×");
b.setBorderPainted(false);
b.setFocusPainted(false);
@@ -61,7 +67,7 @@ public class TabBuilder {
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
urls.remove(c.hashCode());
tabs.remove(c.hashCode());
tab.remove(c);
}
});
@@ -84,11 +90,26 @@ public class TabBuilder {
private static JTabbedPane buildParametersTabbedPane(JTextField urlField) {
JTabbedPane p = new JTabbedPane();
p.add("Params", buildParamsTab(true, urlField));
p.add("Authorization", new JPanel());
p.add("Headers", buildParamsTab(false, null));
JTextArea body = new JTextArea();
indexes.put("main[waiting].body", body);
p.add("Body", body);
// Response tab
JTextArea repHeader = new JTextArea();
repHeader.setBackground(Color.WHITE);
repHeader.setEditable(false);
JScrollPane rep = new JScrollPane(repHeader);
JTextArea reqHeader = new JTextArea();
reqHeader.setBackground(Color.WHITE);
reqHeader.setEditable(false);
JScrollPane req = new JScrollPane(reqHeader);
indexes.put("main[waiting].requestHeaderTextArea", reqHeader);
indexes.put("main[waiting].responseHeaderTextArea", repHeader);
p.add("Response", new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
rep,
req
));
return p;
}
@@ -100,19 +121,36 @@ public class TabBuilder {
return (JTextArea) indexes.get("main[" + index + "].body");
}
public static JTextArea getRequestHeaderTextArea(int index) {
return (JTextArea) indexes.get("main[" + index + "].requestHeaderTextArea");
}
public static JTextArea getResponseHeaderTextArea(int index) {
return (JTextArea) indexes.get("main[" + index + "].responseHeaderTextArea");
}
public static HashMap<String, String> getParams(int index) {
JTable t = (JTable) indexes.get("main[" + index + "].params");
return Tools.tableToHashMap(t);
}
public static JTable getParamsTable(int index) {
return (JTable) indexes.get("main[" + index + "].params");
}
public static HashMap<String, String> getHeaders(int index) {
JTable t = (JTable) indexes.get("main[" + index + "].headers");
return Tools.tableToHashMap(t);
}
public static JLabel getLabel(int index) {
return (JLabel) indexes.get("main[" + index + "].tabTitle");
}
private static JPanel buildParamsTab(boolean isParam, JTextField urlField) {
String[] headers = {"Keys", "Values"};
DefaultTableModel model = new DefaultTableModel(new Object[][]{}, headers);
DefaultTableModel model = (isParam)? new ReadOnlyTableModel(new Object[][]{}, headers) :
new DefaultTableModel(new Object[][]{}, headers);
JPanel p = new JPanel();
JTable t = new JTable(model);
indexes.put((isParam) ? "main[waiting].params" : "main[waiting].headers", t);
@@ -120,17 +158,19 @@ public class TabBuilder {
JButton delButton = new JButton("Remove");
delButton.setEnabled(false);
t.getSelectionModel().addListSelectionListener(event -> delButton.setEnabled(t.getSelectedRows().length > 0));
p.add(addButton);
p.add(delButton);
if (isParam) {
addButton.addMouseListener(new NewParamMouseAdapter(t, urlField));
delButton.addMouseListener(new DeleteParamMouseAdapter(t, urlField));
} else {
addButton.addMouseListener(new CustomNewMouseAdapter(t));
delButton.addMouseListener(new CustomDeleteMouseAdapter(t));
JButton authButton = new JButton("Set authorization");
authButton.addMouseListener(new AuthorizationAdapter(t));
p.add(authButton);
}
p.add(addButton);
p.add(delButton);
p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
JPanel pp = new JPanel();
pp.add(p);
JScrollPane sp = new JScrollPane(t);

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ovh.alexisdelhaie.endpoint.configuration.AboutDialog">
<grid id="cbd77" binding="contentPane" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="cbd77" binding="contentPane" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="10" left="10" bottom="10" right="10"/>
<constraints>
<xy x="48" y="54" width="451" height="414"/>
<xy x="48" y="54" width="864" height="414"/>
</constraints>
<properties/>
<border type="none"/>
@@ -11,7 +11,7 @@
<grid id="e3588" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
@@ -58,6 +58,18 @@
</component>
</children>
</grid>
<component id="21f05" class="javax.swing.JLabel" binding="banner">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="1" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<horizontalAlignment value="2"/>
<horizontalTextPosition value="2"/>
<text value=""/>
<verticalAlignment value="1"/>
<verticalTextPosition value="1"/>
</properties>
</component>
</children>
</grid>
</form>

View File

@@ -1,20 +1,24 @@
package ovh.alexisdelhaie.endpoint.configuration;
import ovh.alexisdelhaie.endpoint.MainWindow;
import ovh.alexisdelhaie.endpoint.utils.Tools;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
public class AboutDialog extends JDialog {
public static final int WIDTH = 450;
public static final int WIDTH = 740;
public static final int HEIGHT = 500;
public static final String VERSION = "0.1.3";
public static final String VERSION = "0.1.5.3";
private JPanel contentPane;
private JLabel version;
private JLabel javaVersion;
private JLabel banner;
private AboutDialog() {
setContentPane(contentPane);
@@ -22,6 +26,11 @@ public class AboutDialog extends JDialog {
setTitle("About EndPoint");
version.setText("Version: " + VERSION + " (NOT FINISHED)");
javaVersion.setText("Software: Java " + System.getProperty("java.version") + " (GUI: Java Swing)");
try {
banner.setIcon(new ImageIcon(ImageIO.read(MainWindow.class.getResource("/banner.png"))));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void showDialog() {

View File

@@ -49,7 +49,7 @@
</component>
</children>
</grid>
<grid id="e3588" layout-manager="GridLayoutManager" row-count="7" column-count="5" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="e3588" layout-manager="GridLayoutManager" row-count="9" column-count="5" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@@ -73,7 +73,7 @@
</hspacer>
<vspacer id="d1324">
<constraints>
<grid row="6" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
<grid row="8" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="c7f68" class="javax.swing.JLabel">
@@ -168,6 +168,40 @@
</constraints>
<properties/>
</component>
<component id="ff47a" class="javax.swing.JLabel">
<constraints>
<grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<font style="1"/>
<text value="Appearance"/>
</properties>
</component>
<component id="e33fb" class="javax.swing.JLabel">
<constraints>
<grid row="7" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Theme"/>
</properties>
</component>
<hspacer id="1d971">
<constraints>
<grid row="7" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="d304c" class="javax.swing.JComboBox" binding="theme">
<constraints>
<grid row="7" column="2" row-span="1" col-span="3" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<model>
<item value="IntelliJ"/>
<item value="Darcula"/>
<item value="System"/>
</model>
</properties>
</component>
</children>
</grid>
</children>

View File

@@ -1,9 +1,11 @@
package ovh.alexisdelhaie.endpoint.configuration;
import ovh.alexisdelhaie.endpoint.utils.MessageDialog;
import ovh.alexisdelhaie.endpoint.utils.Tools;
import javax.swing.*;
import java.awt.*;
import java.util.Objects;
public class ConfigurationDialog extends JDialog {
@@ -17,10 +19,11 @@ public class ConfigurationDialog extends JDialog {
private JComboBox<String> httpVersion;
private JSpinner timeout;
private JButton aboutButton;
private JComboBox theme;
private final ConfigurationProperties props;
private ConfigurationDialog(ConfigurationProperties props) {
private ConfigurationDialog(ConfigurationProperties props, JFrame frame) {
setContentPane(contentPane);
setModal(true);
getRootPane().setDefaultButton(buttonOK);
@@ -32,6 +35,7 @@ public class ConfigurationDialog extends JDialog {
allowDowngrade.setSelected(this.props.getBooleanProperty("allowDowngrade", true));
httpVersion.setSelectedItem(this.props.getStringProperty("httpVersion", "HTTP/1.0"));
timeout.setValue(this.props.getIntegerProperty("timeout", 10000));
theme.setSelectedItem(this.props.getStringProperty("theme", "IntelliJ"));
allowInvalidSsl.addActionListener((e) -> {
this.props.setProperty("allowInvalidSsl", String.valueOf(allowInvalidSsl.isSelected()));
@@ -45,15 +49,24 @@ public class ConfigurationDialog extends JDialog {
timeout.addChangeListener((e) -> {
this.props.setProperty("timeout", String.valueOf(timeout.getValue()));
});
theme.addActionListener((e) -> {
String value = (String) theme.getSelectedItem();
this.props.setProperty("theme", value);
try {
UIManager.setLookAndFeel(Tools.getLookAndFeel(Objects.requireNonNull(value)));
SwingUtilities.updateComponentTreeUI(frame);
} catch(UnsupportedLookAndFeelException | InstantiationException | IllegalAccessException | ClassNotFoundException err) {
MessageDialog.error("Error while changing theme", err.getMessage());
}
});
}
private void onOK() {
dispose();
}
public static void showDialog(ConfigurationProperties props) {
ConfigurationDialog dialog = new ConfigurationDialog(props);
public static void showDialog(ConfigurationProperties props, JFrame frame) {
ConfigurationDialog dialog = new ConfigurationDialog(props, frame);
dialog.setModal(true);
dialog.setMinimumSize(new Dimension(WIDTH, HEIGHT));
dialog.setMaximumSize(new Dimension(WIDTH, HEIGHT));

View File

@@ -1,6 +1,7 @@
package ovh.alexisdelhaie.endpoint.http;
import ovh.alexisdelhaie.endpoint.configuration.ConfigurationProperties;
import ovh.alexisdelhaie.endpoint.utils.MessageDialog;
import javax.net.ssl.*;
import java.io.BufferedInputStream;
@@ -88,6 +89,8 @@ public class HttpClient {
long time = end.toEpochMilli() - start.toEpochMilli();
r.setRawRequest(request);
return Optional.of(new Response(b, time, downgraded, r));
} catch (Exception e) {
MessageDialog.error("HTTP Error", e.getMessage());
} finally {
s.close();
}
@@ -126,7 +129,6 @@ public class HttpClient {
}
SSLSocket s = (SSLSocket) factory.createSocket(host, port);
s.setEnabledProtocols(new String[] { "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2" });
s.setKeepAlive(false);
s.setSoTimeout(timeout);
if (allowDowngrade) {

View File

@@ -29,14 +29,18 @@ public class Response {
headers = new HashMap<>();
rawResponse = new String(res, StandardCharsets.UTF_8);
int crlf = rawResponse.indexOf(DOUBLE_CRLF);
if (crlf > 0) {
rawHeaders = rawResponse.substring(0, crlf);
parseHeaders();
rawResponse = new String(res, getEncoding());
body = rawResponse.substring(crlf + DOUBLE_CRLF.length());
parseBody();
} else {
rawHeaders = rawResponse;
}
this.time = time;
request = r;
this.downgraded = downgraded;
parseBody();
}
private void parseHeaders() {
@@ -51,13 +55,13 @@ public class Response {
}
private void parseStatus(String l) {
Pattern p = Pattern.compile("^(HTTP/1.1)\\s([0-9]{3})\\s(.+)$");
Pattern p = Pattern.compile("^(HTTP/[0-2].[0-1])\\s([0-9]{3})\\s(.+)$");
Matcher m = p.matcher(l);
if (m.matches()) {
statusCode = Integer.parseInt(m.group(2));
status = m.group(3);
} else {
p = Pattern.compile("^(HTTP/1.1)\\s([0-9]{3})?(.*)$");
p = Pattern.compile("^(HTTP/[0-2].[0-1])\\s([0-9]{3})?(.*)$");
m = p.matcher(l);
if (m.matches()) {
statusCode = Integer.parseInt(m.group(2));
@@ -73,12 +77,7 @@ public class Response {
private void parseBody() {
if (headers.containsKey("transfer-encoding")) {
if (headers.get("transfer-encoding").toLowerCase().contains("chunked")) {
ArrayList<String> chunks = Chunked.parse(body);
final StringBuilder sb = new StringBuilder();
for (String chunk : chunks) {
sb.append(chunk);
}
body = sb.toString();
body = Chunked.parse(body);
}
}
}

View File

@@ -1,23 +1,22 @@
package ovh.alexisdelhaie.endpoint.http.parsers;
import org.apache.commons.httpclient.ChunkedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public class Chunked {
public static ArrayList<String> parse(String body) {
ArrayList<String> result = new ArrayList<>();
public static String parse(String body) {
String result;
try {
body = body.strip();
int length; int pos;
do {
pos = body.indexOf("\r\n");
length = Integer.parseInt(body.substring(0, pos), 16);
result.add(body.substring(pos + 2, length));
body = body.substring(pos + 2 + length);
} while (!body.isEmpty());
} catch (NumberFormatException e) {
result.add(body);
ChunkedInputStream inputStream = new ChunkedInputStream(new ByteArrayInputStream(body.getBytes()));
result = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
result = e.getLocalizedMessage();
}
return result;
}

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ovh.alexisdelhaie.endpoint.utils.AuthorizationDialog">
<grid id="cbd77" binding="contentPane" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="10" left="10" bottom="10" right="10"/>
<constraints>
<xy x="48" y="54" width="436" height="297"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="94766" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<hspacer id="98af6">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<grid id="9538f" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="true" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="e7465" class="javax.swing.JButton" binding="buttonOK">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="OK"/>
</properties>
</component>
<component id="5723f" class="javax.swing.JButton" binding="buttonCancel">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Cancel"/>
</properties>
</component>
</children>
</grid>
</children>
</grid>
<grid id="e3588" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="83a13" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<font size="24"/>
<text value="Authorization"/>
</properties>
</component>
<hspacer id="b508b">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<grid id="7da3c" binding="bearerPanel" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="3" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="f9a92" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Token"/>
</properties>
</component>
<component id="1c1f0" class="javax.swing.JTextField" binding="bearerTokenField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
</children>
</grid>
<component id="1107a" class="javax.swing.JComboBox" binding="typeBox">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<model>
<item value="Bearer"/>
<item value="Basic"/>
</model>
</properties>
</component>
<component id="f7e01" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Type"/>
</properties>
</component>
<grid id="d162d" binding="basicPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<visible value="false"/>
</properties>
<border type="none"/>
<children>
<component id="79b35" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Username"/>
</properties>
</component>
<component id="a6880" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Password"/>
</properties>
</component>
<component id="ae39c" class="javax.swing.JTextField" binding="usernameBasicField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="26ce0" class="javax.swing.JTextField" binding="passwordBasicField">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
</children>
</grid>
</children>
</grid>
</children>
</grid>
</form>

View File

@@ -0,0 +1,103 @@
package ovh.alexisdelhaie.endpoint.utils;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Objects;
import java.util.Optional;
public class AuthorizationDialog extends JDialog {
public static final int WIDTH = 325;
public static final int HEIGHT = 220;
private JPanel contentPane;
private JButton buttonOK;
private JButton buttonCancel;
private JComboBox<String> typeBox;
private JPanel bearerPanel;
private JTextField bearerTokenField;
private JTextField usernameBasicField;
private JTextField passwordBasicField;
private JPanel basicPanel;
private boolean accepted = false;
private String finalValue = "";
public AuthorizationDialog() {
setTitle("Set authorization");
setContentPane(contentPane);
setModal(true);
getRootPane().setDefaultButton(buttonOK);
buttonOK.addActionListener(e -> onOK());
buttonCancel.addActionListener(e -> onCancel());
typeBox.addItemListener((e) -> {
String type = (String) typeBox.getSelectedItem();
switch (Objects.requireNonNull(type)) {
case "Basic" -> {
bearerPanel.setVisible(false);
basicPanel.setVisible(true);
}
case "Bearer" -> {
basicPanel.setVisible(false);
bearerPanel.setVisible(true);
}
}
});
// call onCancel() when cross is clicked
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
onCancel();
}
});
// call onCancel() on ESCAPE
contentPane.registerKeyboardAction(new ActionListener() {
public void actionPerformed(ActionEvent e) {
onCancel();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
}
private void onOK() {
String type = (String) typeBox.getSelectedItem();
switch (Objects.requireNonNull(type)) {
case "Basic" -> finalValue = String.format(
"Basic %s",
Tools.toBase64(
String.format(
"%s:%s",
usernameBasicField.getText(),
passwordBasicField.getText()
)
)
);
case "Bearer" -> finalValue = String.format("Bearer %s", bearerTokenField.getText());
}
accepted = true;
dispose();
}
private void onCancel() {
dispose();
}
public static Optional<KeyValuePair> showDialog() {
AuthorizationDialog dialog = new AuthorizationDialog();
dialog.setModal(true);
dialog.setMinimumSize(new Dimension(WIDTH, HEIGHT));
dialog.setMaximumSize(new Dimension(WIDTH, HEIGHT));
dialog.setResizable(false);
Tools.centerFrame(dialog);
dialog.setVisible(true);
if (dialog.accepted) {
return Optional.of(new KeyValuePair("authorization", dialog.finalValue));
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,55 @@
package ovh.alexisdelhaie.endpoint.utils;
import ovh.alexisdelhaie.endpoint.http.Response;
public class RequestTab {
private String url;
private boolean running;
private Response res;
private String method;
public RequestTab(String method) {
url = "";
running = false;
res = null;
this.method = method;
}
public String getUrl() {
return url;
}
public RequestTab setUrl(String url) {
this.url = url;
return this;
}
public boolean isRunning() {
return running;
}
public RequestTab setRunning(boolean running) {
this.running = running;
return this;
}
public Response getRes() {
return res;
}
public RequestTab setRes(Response res) {
this.res = res;
return this;
}
public String getMethod() {
return method;
}
public RequestTab setMethod(String method) {
this.method = method;
return this;
}
}

View File

@@ -1,8 +1,13 @@
package ovh.alexisdelhaie.endpoint.utils;
import com.formdev.flatlaf.FlatDarculaLaf;
import com.formdev.flatlaf.FlatIntelliJLaf;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
public class Tools {
@@ -32,4 +37,21 @@ public class Tools {
return result;
}
public static String toBase64(String decoded) {
byte[] encodedBytes = Base64.getEncoder().encode(decoded.getBytes());
return new String(encodedBytes);
}
public static String getLookAndFeel(String theme) {
switch (theme) {
case "IntelliJ" -> {
return FlatIntelliJLaf.class.getName();
}
case "Darcula" -> {
return FlatDarculaLaf.class.getName();
}
}
return UIManager.getSystemLookAndFeelClassName();
}
}

View File

@@ -0,0 +1,36 @@
package ovh.alexisdelhaie.endpoint.utils.adapter;
import ovh.alexisdelhaie.endpoint.utils.AuthorizationDialog;
import ovh.alexisdelhaie.endpoint.utils.KeyValuePair;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Optional;
public class AuthorizationAdapter extends MouseAdapter {
protected final JTable table;
public AuthorizationAdapter(JTable table) {
this.table = table;
}
@Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
Optional<KeyValuePair> result = AuthorizationDialog.showDialog();
if (result.isPresent()) {
DefaultTableModel m = (DefaultTableModel) table.getModel();
for (int i = m.getRowCount() - 1; i > -1; i--) {
String key = (String) m.getValueAt(i,0);
if (key.toLowerCase().equals(result.get().getKey().toLowerCase())) {
m.removeRow(i);
}
}
m.addRow(new Object[]{result.get().getKey(), result.get().getValue()});
}
}
}

View File

@@ -28,6 +28,12 @@ public class CustomNewMouseAdapter extends MouseAdapter {
Optional<KeyValuePair> result = InsertToTableDialog.showDialog("Enter value");
if (result.isPresent()) {
DefaultTableModel m = (DefaultTableModel) table.getModel();
for (int i = m.getRowCount() - 1; i > -1; i--) {
String key = (String) m.getValueAt(i,0);
if (key.toLowerCase().equals(result.get().getKey().toLowerCase())) {
m.removeRow(i);
}
}
m.addRow(new Object[]{result.get().getKey(), result.get().getValue()});
valid = true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB