Browse Source

add sparrow wallet file export, dont unnecessarily ask for password when exporting

terminal
Craig Raw 4 years ago
parent
commit
b74741bccb
  1. 2
      build.gradle
  2. 4
      src/main/java/com/sparrowwallet/sparrow/AppServices.java
  3. 29
      src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java
  4. 4
      src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java
  5. 7
      src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java
  6. 17
      src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java
  7. 7
      src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java
  8. 17
      src/main/java/com/sparrowwallet/sparrow/io/Electrum.java
  9. 62
      src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java
  10. 7
      src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java
  11. 17
      src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java
  12. 3
      src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java
  13. BIN
      src/main/resources/image/sparrow-large.png
  14. BIN
      src/main/resources/image/sparrow.png
  15. BIN
      src/main/resources/image/sparrow@2x.png
  16. BIN
      src/main/resources/image/sparrow@3x.png

2
build.gradle

@ -115,7 +115,7 @@ run {
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"] "--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
if(os.macOsX) { if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"] "--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
} }
} }

4
src/main/java/com/sparrowwallet/sparrow/AppServices.java

@ -334,7 +334,7 @@ public class AppServices {
stage.setMinWidth(650); stage.setMinWidth(650);
stage.setMinHeight(730); stage.setMinHeight(730);
stage.setScene(scene); stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png"))); stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
appController.initializeView(); appController.initializeView();
stage.show(); stage.show();
@ -453,7 +453,7 @@ public class AppServices {
public static void setStageIcon(Window window) { public static void setStageIcon(Window window) {
Stage stage = (Stage)window; Stage stage = (Stage)window;
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow.png"))); stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow-large.png")));
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) { if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm()); stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());

29
src/main/java/com/sparrowwallet/sparrow/control/FileWalletExportPane.java

@ -81,7 +81,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File"); fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension()); String extension = exporter.getExportFileExtension(wallet);
fileChooser.setInitialFileName(wallet.getName() + "-" +
exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "") +
(extension == null || extension.isEmpty() ? "" : "." + extension));
File file = fileChooser.showSaveDialog(window); File file = fileChooser.showSaveDialog(window);
if(file != null) { if(file != null) {
@ -90,9 +93,8 @@ public class FileWalletExportPane extends TitledDescriptionPane {
} }
private void exportWallet(File file) { private void exportWallet(File file) {
Wallet copy = wallet.copy(); if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
Wallet copy = wallet.copy();
if(copy.isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait(); Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) { if(password.isPresent()) {
@ -101,7 +103,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
decryptWalletService.setOnSucceeded(workerStateEvent -> { decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done")); EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue(); Wallet decryptedWallet = decryptWalletService.getValue();
exportWallet(file, decryptedWallet);
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
}); });
decryptWalletService.setOnFailed(workerStateEvent -> { decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed")); EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
@ -111,19 +118,19 @@ public class FileWalletExportPane extends TitledDescriptionPane {
decryptWalletService.start(); decryptWalletService.start();
} }
} else { } else {
exportWallet(file, copy); exportWallet(file, wallet);
} }
} }
private void exportWallet(File file, Wallet decryptedWallet) { private void exportWallet(File file, Wallet exportWallet) {
try { try {
if(file != null) { if(file != null) {
OutputStream outputStream = new FileOutputStream(file); OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(decryptedWallet, outputStream); exporter.exportWallet(exportWallet, outputStream);
EventManager.get().post(new WalletExportEvent(decryptedWallet)); EventManager.get().post(new WalletExportEvent(exportWallet));
} else { } else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(decryptedWallet, outputStream); exporter.exportWallet(exportWallet, outputStream);
QRDisplayDialog qrDisplayDialog; QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) { if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true); qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
@ -138,8 +145,6 @@ public class FileWalletExportPane extends TitledDescriptionPane {
errorMessage = e.getCause().getMessage(); errorMessage = e.getCause().getMessage();
} }
setError("Export Error", errorMessage); setError("Export Error", errorMessage);
} finally {
decryptedWallet.clearPrivate();
} }
} }
} }

4
src/main/java/com/sparrowwallet/sparrow/control/WalletExportDialog.java

@ -41,9 +41,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters; List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) { if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop()); exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) { } else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig()); exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
} else { } else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
} }

7
src/main/java/com/sparrowwallet/sparrow/io/CoboVaultSinglesig.java

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
@ -14,6 +16,8 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport { public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
private static final Logger log = LoggerFactory.getLogger(CoboVaultSinglesig.class);
@Override @Override
public String getName() { public String getName() {
return "Cobo Vault"; return "Cobo Vault";
@ -53,7 +57,8 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
return keystore; return keystore;
} catch (Exception e) { } catch (Exception e) {
throw new ImportException(e); log.error("Error getting Cobo Vault keystore", e);
throw new ImportException("Error getting Cobo Vault keystore", e);
} }
} }

17
src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java

@ -11,6 +11,8 @@ import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -19,6 +21,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
public class ColdcardMultisig implements WalletImport, KeystoreFileImport, WalletExport { public class ColdcardMultisig implements WalletImport, KeystoreFileImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(ColdcardMultisig.class);
@Override @Override
public String getName() { public String getName() {
return "Coldcard Multisig"; return "Coldcard Multisig";
@ -81,7 +85,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
} }
@Override @Override
public String getExportFileExtension() { public String getExportFileExtension(Wallet wallet) {
return "txt"; return "txt";
} }
@ -146,7 +150,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
return wallet; return wallet;
} catch(Exception e) { } catch(Exception e) {
throw new ImportException(e); log.error("Error importing Coldcard multisig wallet", e);
throw new ImportException("Error importing Coldcard multisig wallet", e);
} }
} }
@ -199,7 +204,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
writer.flush(); writer.flush();
writer.close(); writer.close();
} catch(Exception e) { } catch(Exception e) {
throw new ExportException(e); log.error("Error exporting Coldcard multisig wallet", e);
throw new ExportException("Error exporting Coldcard multisig wallet", e);
} }
} }
@ -227,4 +233,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
public boolean isWalletExportScannable() { public boolean isWalletExportScannable() {
return false; return false;
} }
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
} }

7
src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java

@ -9,6 +9,8 @@ import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
@ -18,6 +20,8 @@ import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
public class ColdcardSinglesig implements KeystoreFileImport, WalletImport { public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
private static final Logger log = LoggerFactory.getLogger(ColdcardSinglesig.class);
@Override @Override
public String getName() { public String getName() {
return "Coldcard"; return "Coldcard";
@ -82,7 +86,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
} }
} }
} catch (Exception e) { } catch (Exception e) {
throw new ImportException(e); log.error("Error getting Coldcard keystore", e);
throw new ImportException("Error getting Coldcard keystore", e);
} }
throw new ImportException("Correct derivation not found for script type: " + scriptType); throw new ImportException("Correct derivation not found for script type: " + scriptType);

17
src/main/java/com/sparrowwallet/sparrow/io/Electrum.java

@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@ -22,6 +24,8 @@ import java.util.*;
import java.util.zip.InflaterInputStream; import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport { public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(Electrum.class);
@Override @Override
public String getName() { public String getName() {
return "Electrum"; return "Electrum";
@ -246,7 +250,8 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
return wallet; return wallet;
} catch (Exception e) { } catch (Exception e) {
throw new ImportException(e); log.error("Error importing Electrum Wallet", e);
throw new ImportException("Error importing Electrum Wallet", e);
} }
} }
@ -273,7 +278,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
} }
@Override @Override
public String getExportFileExtension() { public String getExportFileExtension(Wallet wallet) {
return "json"; return "json";
} }
@ -350,7 +355,8 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
outputStream.flush(); outputStream.flush();
outputStream.close(); outputStream.close();
} catch (Exception e) { } catch (Exception e) {
throw new ExportException(e); log.error("Error exporting Electrum Wallet", e);
throw new ExportException("Error exporting Electrum Wallet", e);
} }
} }
@ -379,6 +385,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
return "Export this wallet as an Electrum wallet file."; return "Export this wallet as an Electrum wallet file.";
} }
@Override
public boolean walletExportRequiresDecryption() {
return true;
}
private static class ElectrumJsonWallet { private static class ElectrumJsonWallet {
public Map<String, ElectrumKeystore> keystores = new LinkedHashMap<>(); public Map<String, ElectrumKeystore> keystores = new LinkedHashMap<>();
public String wallet_type; public String wallet_type;

62
src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java

@ -0,0 +1,62 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.OutputStream;
import java.nio.file.Files;
public class Sparrow implements WalletExport {
private static final Logger log = LoggerFactory.getLogger(Sparrow.class);
@Override
public String getName() {
return "Sparrow";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SPARROW;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Files.copy(storage.getWalletFile().toPath(), outputStream);
outputStream.flush();
outputStream.close();
} catch(Exception e) {
log.error("Error exporting Sparrow wallet file", e);
throw new ExportException("Error exporting Sparrow wallet file", e);
}
}
@Override
public String getWalletExportDescription() {
return "Exports a copy of your Sparrow wallet file, which can be loaded in another Sparrow instance running on any supported platform.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(storage != null && (storage.getEncryptionPubKey() == null || Storage.NO_PASSWORD_KEY.equals(storage.getEncryptionPubKey()))) {
return "json";
}
return "";
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
}

7
src/main/java/com/sparrowwallet/sparrow/io/SpecterDIY.java

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -15,6 +17,8 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class SpecterDIY implements KeystoreFileImport { public class SpecterDIY implements KeystoreFileImport {
private static final Logger log = LoggerFactory.getLogger(SpecterDIY.class);
@Override @Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try { try {
@ -34,7 +38,8 @@ public class SpecterDIY implements KeystoreFileImport {
return keystore; return keystore;
} catch(IOException e) { } catch(IOException e) {
throw new ImportException(e); log.error("Error getting Specter DIY keystore", e);
throw new ImportException("Error getting Specter DIY keystore", e);
} }
} }

17
src/main/java/com/sparrowwallet/sparrow/io/SpecterDesktop.java

@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.InvalidWalletException; import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
@ -15,6 +17,8 @@ import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class SpecterDesktop implements WalletImport, WalletExport { public class SpecterDesktop implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
@Override @Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException { public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try { try {
@ -29,7 +33,8 @@ public class SpecterDesktop implements WalletImport, WalletExport {
outputStream.flush(); outputStream.flush();
outputStream.close(); outputStream.close();
} catch(Exception e) { } catch(Exception e) {
throw new ExportException(e); log.error("Error exporting Specter Desktop wallet", e);
throw new ExportException("Error exporting Specter Desktop wallet", e);
} }
} }
@ -39,7 +44,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
} }
@Override @Override
public String getExportFileExtension() { public String getExportFileExtension(Wallet wallet) {
return "json"; return "json";
} }
@ -68,7 +73,8 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return wallet; return wallet;
} }
} catch(Exception e) { } catch(Exception e) {
throw new ImportException(e); log.error("Error importing Specter Desktop wallet", e);
throw new ImportException("Error importing Specter Desktop wallet", e);
} }
throw new ImportException("File was not a valid Specter Desktop wallet"); throw new ImportException("File was not a valid Specter Desktop wallet");
@ -99,6 +105,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return WalletModel.SPECTER_DESKTOP; return WalletModel.SPECTER_DESKTOP;
} }
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
public static class SpecterWallet { public static class SpecterWallet {
public String label; public String label;
public Integer blockheight; public Integer blockheight;

3
src/main/java/com/sparrowwallet/sparrow/io/WalletExport.java

@ -7,6 +7,7 @@ import java.io.OutputStream;
public interface WalletExport extends Export { public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException; void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription(); String getWalletExportDescription();
String getExportFileExtension(); String getExportFileExtension(Wallet wallet);
boolean isWalletExportScannable(); boolean isWalletExportScannable();
boolean walletExportRequiresDecryption();
} }

BIN
src/main/resources/image/sparrow-large.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
src/main/resources/image/sparrow.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
src/main/resources/image/sparrow@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/main/resources/image/sparrow@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Loading…
Cancel
Save