added some dbms logos + count nulls/non-nulls in condition editor

This commit is contained in:
Ralf Wisser
2021-08-24 12:50:25 +02:00
parent 198275c61a
commit 94ffaede10
16 changed files with 175 additions and 78 deletions
@@ -2063,13 +2063,13 @@ public class SubsettingEngine {
}
// TODO "optimierung", 1. im Header "deferred collections" ausweisen + hinweis, dass am Ende die Quittung komplet steht.
// TODO intra database: Quittung bei optimierung am ende nochmal schreiben.
// TODO bei "Delete" keine "deferred collections"
// TODO aber: TODO weil auch bei delete optim.potential (z.b. single-table-export ohne jede assoc.)
// TODO cli argument "-minimize-collections-optimation on|off" (oder besseren namen)
// TODO API berücksichtigen
// TODO export mit optimierung in stats (* 128? 256?) ((`s6`) % 1000) max 180, anderes sx?
// TODO defer working table creation too, maybe they are don't needed
// intra database: Quittung bei optimierung am ende nochmal schreiben.
// bei "Delete" keine "deferred collections"
// aber: zutun weil auch bei delete optim.potential (z.b. single-table-export ohne jede assoc.)
// cli argument "-minimize-collections-optimation on|off" (oder besseren namen)
// API berücksichtigen
// export mit optimierung in stats (* 128? 256?) ((`s6`) % 1000) max 180, anderes sx?
//
// defer working table creation too, maybe they are don't needed
}
@@ -28,7 +28,6 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
@@ -69,7 +68,6 @@ import net.sf.jailer.ui.commandline.UICommandLine;
import net.sf.jailer.ui.databrowser.metadata.MetaDataPanel;
import net.sf.jailer.ui.util.UISettings;
import net.sf.jailer.util.ClasspathUtil;
import net.sf.jailer.util.CsvFile;
import net.sf.jailer.util.CsvFile.Line;
import net.sf.jailer.util.Pair;
@@ -175,6 +173,7 @@ public class DbConnectionDialog extends javax.swing.JDialog {
private Font normal = new Font(font.getName(), font.getStyle() & ~Font.BOLD, font.getSize());
private Font bold = new Font(font.getName(), font.getStyle() | Font.BOLD, font.getSize());
private Map<String, Date> aliasTimestamp = new HashMap<String, Date>();
private boolean ok;
/**
* Gets connection to DB.
@@ -184,6 +183,7 @@ public class DbConnectionDialog extends javax.swing.JDialog {
public boolean connect(String reason, boolean keepState) {
boolean oldIsConnected = isConnected;
ConnectionInfo oldCurrentConnection = currentConnection;
ok = false;
try {
if (!located) {
pack();
@@ -206,7 +206,7 @@ public class DbConnectionDialog extends javax.swing.JDialog {
if (currentConnection == null) {
isConnected = false;
}
return isConnected;
return isConnected && ok;
} finally {
if (keepState && !isConnected) {
isConnected = oldIsConnected;
@@ -321,6 +321,12 @@ public class DbConnectionDialog extends javax.swing.JDialog {
}
((JLabel) render).setToolTipText(String.valueOf(value));
render.setFont(column == 0? bold : normal);
((JLabel) render).setIcon(null);
if (value instanceof String && ((String) value).startsWith("*")) {
((JLabel) render).setToolTipText(null);
((JLabel) render).setText(null);
((JLabel) render).setIcon(UIUtil.scaleIcon(((JLabel) render), UIUtil.readImage(((String) value).substring(1), false), 1.5));
}
return render;
}
});
@@ -410,6 +416,7 @@ public class DbConnectionDialog extends javax.swing.JDialog {
}
});
connectionsTable.setRowHeight((int) (connectionsTable.getRowHeight() * 1.5));
refresh();
}
@@ -422,13 +429,15 @@ public class DbConnectionDialog extends javax.swing.JDialog {
int i = 0;
for (ConnectionInfo ci: connectionList) {
Pair<String, Long> modelDetails = DataModelManager.getModelDetails(ci.dataModelFolder, executionContext);
String dbmsLogoUrl = UIUtil.getDBMSLogoURL(ci.url);
String img = dbmsLogoUrl == null? "": ("*" + dbmsLogoUrl);
if (showOnlyRecentyUsedConnections) {
data[i++] = new Object[] { ci.alias, ci.user, ci.url, ci.dataModelFolder == null? "Default" : modelDetails == null? "" : modelDetails.a, UIUtil.toDateAsString(aliasTimestamp.get(ci.alias)) };
data[i++] = new Object[] { img, ci.alias, ci.user, ci.url, ci.dataModelFolder == null? "Default" : modelDetails == null? "" : modelDetails.a, UIUtil.toDateAsString(aliasTimestamp.get(ci.alias)) };
} else {
data[i++] = new Object[] { ci.alias, ci.user, ci.url, ci.dataModelFolder == null? "Default" : modelDetails == null? "" : modelDetails.a };
data[i++] = new Object[] { img, ci.alias, ci.user, ci.url, ci.dataModelFolder == null? "Default" : modelDetails == null? "" : modelDetails.a };
}
}
DefaultTableModel tableModel = new DefaultTableModel(data, !showOnlyRecentyUsedConnections? new String[] { "Name", "User", "URL", "Data Model" } : new String[] { "Name", "User", "URL", "Data Model", "Time" }) {
DefaultTableModel tableModel = new DefaultTableModel(data, !showOnlyRecentyUsedConnections? new String[] { "DBMS", "Name", "User", "URL", "Data Model" } : new String[] { "Name", "User", "URL", "Data Model", "Time" }) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
@@ -959,22 +968,14 @@ public class DbConnectionDialog extends javax.swing.JDialog {
private void newButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newButtonActionPerformed
ConnectionInfo ci = new ConnectionInfo(executionContext);
final String DRIVERLIST_FILE = "driverlist.csv";
File csvFile = Environment.newWorkingFolderFile(DRIVERLIST_FILE);
List<Line> lines = UIUtil.loadDriverList(this);
try {
// check existence of "driverlist.csv"
FileInputStream is = new FileInputStream(csvFile);
is.close();
CsvFile drivers = new CsvFile(csvFile);
List<Line> lines = new ArrayList<Line>(drivers.getLines());
Component root = SwingUtilities.getWindowAncestor(mainPanel);
if (root == null) {
root = mainPanel;
}
Pair<String, String> s = new DbConnectionSettings(root).edit(lines);
if (s == null) return;
for (Line line: lines) {
if (line.cells.get(0).equals(s.a)) {
@@ -994,42 +995,6 @@ public class DbConnectionDialog extends javax.swing.JDialog {
break;
}
}
} catch (FileNotFoundException e) {
StringBuilder info = new StringBuilder();
List<String> fileList = new ArrayList<String>();
try {
info.append(csvFile.getAbsolutePath() + ": ");
File[] files = csvFile.getAbsoluteFile().getParentFile().listFiles();
if (files == null) {
info.append("null");
} else {
for (File file: files) {
String ord;
String name = file.getName();
if (!file.exists()) {
ord = "1";
} else {
ord = "2";
}
int state = 0;
state += file.exists()? 1 : 0;
state += file.isFile()? 2 : 0;
state += file.isDirectory()? 4 : 0;
fileList.add(ord + name + "/" + Integer.toHexString(state));
if (DRIVERLIST_FILE.equalsIgnoreCase(name)) {
ord = "0";
fileList.add(ord + "!" + name + "/" + Integer.toHexString(state));
}
}
}
Collections.sort(fileList);
for (int i = 0; i < fileList.size(); ++i) {
fileList.set(i, fileList.get(i).substring(1));
}
} catch (Throwable t) {
info.append(" err: " + t.getMessage() + ": ");
}
UIUtil.showException(this, "Error", new FileNotFoundException(e.getMessage() + " / (" + info + fileList + ")"));
} catch (Exception e) {
UIUtil.showException(this, "Error", e);
}
@@ -1105,6 +1070,7 @@ public class DbConnectionDialog extends javax.swing.JDialog {
UIUtil.setWaitCursor(root);
if (testConnection(mainPanel, currentConnection, null)) {
isConnected = true;
ok = true;
executionContext.setCurrentConnectionAlias(currentConnection.alias);
onConnect(currentConnection);
if (currentConnection.alias != null && !"".equals(currentConnection.alias)) {
@@ -1399,4 +1365,8 @@ public class DbConnectionDialog extends javax.swing.JDialog {
warnIcon = UIUtil.readImage("/wanr.png");
}
// TODO make field "driver-class" initially empty and optional. If empty -> derive it from driver.csv (always, not only at end of dialog)
// TODO derive (driver-class and)? libs/downloads libs from url (reuse wizzard/driver.csv knowledge)
}
@@ -266,8 +266,9 @@ public class ExtractionModelEditor extends javax.swing.JPanel {
*
* @param extractionModelFile file containing the model
* @param extractionModelFrame the enclosing frame
* @param dbmsLogo
*/
public ExtractionModelEditor(String extractionModelFile, final ExtractionModelFrame extractionModelFrame, boolean horizontalLayout, String connectionState, String connectionStateToolTip, ExecutionContext executionContext) throws IOException {
public ExtractionModelEditor(String extractionModelFile, final ExtractionModelFrame extractionModelFrame, boolean horizontalLayout, String connectionState, String connectionStateToolTip, ImageIcon dbmsLogo, ExecutionContext executionContext) throws IOException {
this.executionContext = executionContext;
this.extractionModelFrame = extractionModelFrame;
this.extractionModelFile = extractionModelFile;
@@ -873,6 +874,7 @@ public class ExtractionModelEditor extends javax.swing.JPanel {
setOrientation(horizontalLayout);
connectivityState.setText(connectionState);
connectivityState.setToolTipText(connectionStateToolTip);
connectivityState.setIcon(dbmsLogo);
if (dataModel != null) {
String modelname = "Data Model \"" + dataModel.getName() + "\"";
@@ -49,10 +49,12 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
@@ -202,7 +204,7 @@ public class ExtractionModelFrame extends javax.swing.JFrame {
}
};
editorPanel.add(extractionModelEditor = new ExtractionModelEditor(extractionModelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), executionContext), "editor");
editorPanel.add(extractionModelEditor = new ExtractionModelEditor(extractionModelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), getDBMSLogo(), executionContext), "editor");
extractionModelEditor.extractionModelFile = extractionModelFile;
restrictedDependenciesView.refresh();
pack();
@@ -303,6 +305,7 @@ public class ExtractionModelFrame extends javax.swing.JFrame {
connectDb.setSelected(dbConnectionDialog.isConnected);
extractionModelEditor.connectivityState.setText(getConnectivityState());
extractionModelEditor.connectivityState.setToolTipText(getConnectivityStateToolTip());
extractionModelEditor.connectivityState.setIcon(getDBMSLogo());
}
private String getConnectivityState() {
@@ -315,12 +318,25 @@ public class ExtractionModelFrame extends javax.swing.JFrame {
private String getConnectivityStateToolTip() {
if (dbConnectionDialog != null && dbConnectionDialog.isConnected) {
return (dbConnectionDialog.currentConnection.url);
return dbConnectionDialog.currentConnection.url;
} else {
return "offline";
}
}
private ImageIcon getDBMSLogo() {
if (dbConnectionDialog != null && dbConnectionDialog.isConnected) {
String dbmsLogoURL = UIUtil.getDBMSLogoURL(dbConnectionDialog.currentConnection.url);
if (dbmsLogoURL == null) {
return null;
} else {
return UIUtil.scaleIcon(new JLabel(), UIUtil.readImage(dbmsLogoURL, false), 1.2);
}
} else {
return null;
}
}
/**
* Opens the filter editor for a given table.
*
@@ -1709,7 +1725,7 @@ public class ExtractionModelFrame extends javax.swing.JFrame {
return;
}
UIUtil.setWaitCursor(this);
ExtractionModelEditor newModelEditor = new ExtractionModelEditor(modelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), executionContext);
ExtractionModelEditor newModelEditor = new ExtractionModelEditor(modelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), getDBMSLogo(), executionContext);
extractionModelEditor.extractionModelFrame = null;
editorPanel.remove(extractionModelEditor);
extractionModelEditor = null;
@@ -1738,7 +1754,7 @@ public class ExtractionModelFrame extends javax.swing.JFrame {
private void reload() {
try {
UIUtil.setWaitCursor(this);
ExtractionModelEditor newModelEditor = new ExtractionModelEditor(extractionModelEditor.extractionModelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), executionContext);
ExtractionModelEditor newModelEditor = new ExtractionModelEditor(extractionModelEditor.extractionModelFile, this, isHorizontalLayout, getConnectivityState(), getConnectivityStateToolTip(), getDBMSLogo(), executionContext);
extractionModelEditor.extractionModelFrame = null;
editorPanel.remove(extractionModelEditor);
editorPanel.add(extractionModelEditor = newModelEditor, "editor");
@@ -663,14 +663,14 @@ public class StringSearchPanel extends javax.swing.JPanel {
JLabel cl = new JLabel(stringCountLeftPad + " ");
cl.setForeground(new Color(255, 255, 255, 0));
panel.add(cl, gbc);
if (count > 1) {
if (count > 1 || count < 0) {
gbc = new GridBagConstraints();
gbc.gridx = 2;
gbc.gridy = 1;
gbc.weightx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.EAST;
cl = new JLabel(" " + count + " ");
cl = new JLabel(" " + Math.abs(count) + " ");
cl.setForeground(new Color(0, 140, 0));
panel.add(cl, gbc);
}
@@ -1185,7 +1185,7 @@ public class StringSearchPanel extends javax.swing.JPanel {
public void setStringCount(Map<String, Integer> stringCount) {
this.stringCount = stringCount;
this.stringCountLeftPad = null;
stringCount.values().stream().max(Integer::compare).ifPresent(max -> {
stringCount.values().stream().map(Math::abs).max(Integer::compare).ifPresent(max -> {
if (max > 1) {
stringCountLeftPad = " ";
for (int i = String.valueOf(max).length(); i > 0; --i) {
+89
View File
@@ -42,6 +42,7 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
@@ -60,11 +61,13 @@ import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
@@ -118,6 +121,8 @@ import net.sf.jailer.ui.util.HttpUtil;
import net.sf.jailer.ui.util.UISettings;
import net.sf.jailer.util.CancellationException;
import net.sf.jailer.util.CancellationHandler;
import net.sf.jailer.util.CsvFile;
import net.sf.jailer.util.CsvFile.Line;
import net.sf.jailer.util.CycleFinder;
import net.sf.jailer.util.JobManager;
import net.sf.jailer.util.Pair;
@@ -1805,4 +1810,88 @@ public class UIUtil {
}
}
private static List<Line> lines;
public static List<Line> loadDriverList(Window parent) {
if (lines != null) {
return lines;
}
lines = new ArrayList<Line>();
final String DRIVERLIST_FILE = "driverlist.csv";
File csvFile = Environment.newWorkingFolderFile(DRIVERLIST_FILE);
try {
// check existence of "driverlist.csv"
FileInputStream is = new FileInputStream(csvFile);
is.close();
CsvFile drivers = new CsvFile(csvFile);
lines.addAll(drivers.getLines());
} catch (FileNotFoundException e) {
StringBuilder info = new StringBuilder();
List<String> fileList = new ArrayList<String>();
try {
info.append(csvFile.getAbsolutePath() + ": ");
File[] files = csvFile.getAbsoluteFile().getParentFile().listFiles();
if (files == null) {
info.append("null");
} else {
for (File file: files) {
String ord;
String name = file.getName();
if (!file.exists()) {
ord = "1";
} else {
ord = "2";
}
int state = 0;
state += file.exists()? 1 : 0;
state += file.isFile()? 2 : 0;
state += file.isDirectory()? 4 : 0;
fileList.add(ord + name + "/" + Integer.toHexString(state));
if (DRIVERLIST_FILE.equalsIgnoreCase(name)) {
ord = "0";
fileList.add(ord + "!" + name + "/" + Integer.toHexString(state));
}
}
}
Collections.sort(fileList);
for (int i = 0; i < fileList.size(); ++i) {
fileList.set(i, fileList.get(i).substring(1));
}
} catch (Throwable t) {
info.append(" err: " + t.getMessage() + ": ");
}
if (parent != null) {
UIUtil.showException(parent, "Error", new FileNotFoundException(e.getMessage() + " / (" + info + fileList + ")"));
}
} catch (Exception e) {
if (parent != null) {
UIUtil.showException(parent, "Error", e);
}
}
return lines;
}
public static String getDBMSLogoURL(String url) {
if (!url.matches("jdbc:.+")) {
return null;
}
Optional<Line> result = loadDriverList(null).stream().filter(line -> {
String prefix = line.cells.get(1).replaceFirst("[<\\[].*$", "");
return !prefix.isEmpty() && url.startsWith(prefix);
}).findAny();
if (result.isPresent()) {
String logoUrl = "/dbmslogo/" + result.get().cells.get(6);
String smallLogoUrl = logoUrl.replaceFirst("^(.*)(\\.([^\\.]+))$", "$1_small$2");
if (readImage(smallLogoUrl, false) != null) {
return smallLogoUrl;
}
return logoUrl;
}
return null;
}
}
@@ -1224,6 +1224,12 @@ public class DataBrowser extends javax.swing.JFrame {
ConnectionInfo connection = dbConnectionDialog != null ? dbConnectionDialog.currentConnection : null;
String dburl = connection != null ? (connection.url) : " ";
connectivityState.setToolTipText(dburl);
String dbmsLogoURL = UIUtil.getDBMSLogoURL(dburl);
if (dbmsLogoURL == null) {
connectivityState.setIcon(null);
} else {
connectivityState.setIcon(UIUtil.scaleIcon(connectivityState, UIUtil.readImage(dbmsLogoURL, false), 1.2));
}
dburl = connection != null ? ((connection.user != null && connection.user.trim().length() > 0 && !connection.alias.startsWith(connection.user+ "@")? connection.user + "@" : "") + connection.alias) : " ";
if (dburl.length() > MAX_LENGTH) {
dburl = dburl.substring(0, MAX_LENGTH - 3) + "...";
@@ -550,7 +550,7 @@ public abstract class WhereConditionEditorPanel extends javax.swing.JPanel {
for (int columnIndex = 0; columnIndex < table.getColumns().size(); ++columnIndex) {
for (boolean noAlias: new boolean[] { false, true }) {
Column column = table.getColumns().get(columnIndex);
String valueRegex = "((?:(?:0x(?:\\d|[a-f])+)|(?:.?'(?:[^']|'')*')|(?:\\d|[\\.\\-\\+])+|(?:true|false)|(?:\\w+\\s*\\([^\\)]*\\)))(?:\\s*\\:\\:\\s*(?:\\w+))?)?";
String valueRegex = "((?:(?:0x(?:\\d|[a-f])+)|(?:[^(]?'(?:[^']|'')*')|(?:\\d|[\\.\\-\\+])+|(?:true|false)|(?:\\w+\\s*\\([^\\)]*\\)))(?:\\s*\\:\\:\\s*(?:\\w+))?)?";
String regex = "(?:(?:and\\s+)?" + "(?:\\b" + (tableAlias == null || noAlias? "" : (tableAlias + "\\s*\\.")) + "\\s*))"
+ (noAlias? "(?<!\\.\\s*)" : "")
+ "(" + quoteRE + "?)" + Pattern.quote(Quoting.staticUnquote(column.name)) + "(" + quoteRE
@@ -1494,9 +1494,10 @@ public abstract class WhereConditionEditorPanel extends javax.swing.JPanel {
}
});
finalDistinctExisting.keySet().forEach(s -> {
defaultComboBoxModel.addElement(s);
renderConsumer.put(s, label -> label
.setIcon(UIUtil.scaleIcon(WhereConditionEditorPanel.this, emptyIcon)));
if (!nullPattern.matcher(s).matches()) {
defaultComboBoxModel.addElement(s);
renderConsumer.put(s, label -> label.setIcon(UIUtil.scaleIcon(WhereConditionEditorPanel.this, emptyIcon)));
}
});
distinctExistingModel.clear();
for (int i = 0; i < defaultComboBoxModel.getSize(); ++i) {
@@ -1550,9 +1551,10 @@ public abstract class WhereConditionEditorPanel extends javax.swing.JPanel {
fullSearchCheckbox.setEnabled(false);
}
finalDistinctExistingFull.keySet().forEach(s -> {
defaultComboBoxModel.addElement(s);
renderConsumer.put(s, label -> label
.setIcon(UIUtil.scaleIcon(WhereConditionEditorPanel.this, emptyIcon)));
if (!nullPattern.matcher(s).matches()) {
defaultComboBoxModel.addElement(s);
renderConsumer.put(s, label -> label.setIcon(UIUtil.scaleIcon(WhereConditionEditorPanel.this, emptyIcon)));
}
});
distinctExistingFullModel.clear();
if (fullSearchCheckbox.isEnabled()) {
@@ -1573,7 +1575,7 @@ public abstract class WhereConditionEditorPanel extends javax.swing.JPanel {
}
}
distinctExistingFullModel.addAll(finalDistinctExistingFull.keySet());
finalDistinctExistingFull.keySet().forEach(s -> { if (!nullPattern.matcher(s).matches()) { distinctExistingFullModel.add(s); }});
if (fromCache[0] || fromCacheFull[0]) {
clearCacheButton.setVisible(true);
}
@@ -1773,14 +1775,26 @@ public abstract class WhereConditionEditorPanel extends javax.swing.JPanel {
@Override
public void readCurrentRow(ResultSet resultSet) throws SQLException {
Object obj = getCellContentConverter(resultSet, session, session.dbms).getObject(resultSet, 1);
int rc = resultSet.getInt(2);
if (obj == null) {
withNull[0] = true;
Integer count = result.get("is null");
if (count == null) {
result.put("is null", -rc);
} else {
result.put("is null", count - rc);
}
} else {
Integer count = result.get("is not null");
if (count == null) {
result.put("is not null", -rc);
} else {
result.put("is not null", count - rc);
}
if (cellEditor.isEditable(table, columnIndex, obj)) {
String text = cellEditor.cellContentToText(columnIndex, obj);
int rc = resultSet.getInt(2);
if (text.length() <= MAX_TEXT_LENGTH) {
Integer count = result.get(text);
count = result.get(text);
if (count == null) {
result.put(text, rc);
} else {
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB