diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 64a4986d..0ce8d576 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -256,7 +256,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 diff --git a/CMakeLists.txt b/CMakeLists.txt index b94e7e73..cb695631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.1 +project(${PROJECT} VERSION 4.8.2.4 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2068) +set(APP_ANDROID_VERSION_CODE 2071) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/README.md b/README.md index e4a6bf0c..eed800f5 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,17 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
- - - - - -
- + +[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) [All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
+ ## Features @@ -37,7 +33,7 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website +- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit - [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) - [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 179def86..9e44e022 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -20,7 +20,7 @@ - + diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index e93834f4..80cab96d 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,11 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder -import java.io.IOException -import java.util.Locale +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -27,6 +28,8 @@ open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 protected open val ifName: String = "amn0" + private lateinit var scope: CoroutineScope + private var statusJob: Job? = null override val statistics: Statistics get() { @@ -49,46 +52,17 @@ open class Wireguard : Protocol() { override fun internalInit() { if (!isInitialized) loadSharedLibrary(context, "wg-go") + if (this::scope.isInitialized) { + scope.cancel() + } + scope = CoroutineScope(Dispatchers.IO) } override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) - val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) - waitForConnection(startTime) - state.value = CONNECTED } - private suspend fun waitForConnection(startTime: Long) { - Log.d(TAG, "Waiting for connection") - withContext(Dispatchers.IO) { - val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) - try { - delay(1000) - var log = getLogcat(time) - Log.v(TAG, "First waiting log: $log") - // check that there is a connection log, - // to avoid infinite connection - if (!log.contains("Attaching to interface")) { - Log.w(TAG, "Logs do not contain a connection log") - return@withContext - } - while (!log.contains("Received handshake response")) { - delay(1000) - log = getLogcat(time) - } - } catch (e: IOException) { - Log.e(TAG, "Failed to get logcat: $e") - } - } - } - - private fun getLogcat(time: String): String = - ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) - .redirectErrorStream(true) - .start() - .inputStream.reader().readText() - protected open fun parseConfig(config: JSONObject): WireguardConfig { val configData = config.getJSONObject("wireguard_config_data") return WireguardConfig.build { @@ -178,6 +152,43 @@ open class Wireguard : Protocol() { tunnelHandle = -1 throw VpnStartException("Protect VPN interface: permission not granted or revoked") } + launchStatusJob() + } + + private fun launchStatusJob() { + Log.d(TAG, "Launch status job") + statusJob = scope.launch { + while (true) { + val lastHandshake = getLastHandshake() + Log.v(TAG, "lastHandshake=$lastHandshake") + if (lastHandshake == 0L) { + delay(1000) + continue + } + if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED + else if (lastHandshake == -1L) state.value = DISCONNECTED + statusJob = null + break + } + } + } + + private fun getLastHandshake(): Long { + if (tunnelHandle == -1) { + Log.e(TAG, "Trying to get config of a non-existent tunnel") + return -1 + } + val config = GoBackend.awgGetConfig(tunnelHandle) + if (config == null) { + Log.e(TAG, "Failed to get tunnel config") + return -2 + } + val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + if (lastHandshake == null) { + Log.e(TAG, "Failed to get last_handshake_time_sec") + return -2 + } + return lastHandshake } override fun stopVpn() { @@ -185,6 +196,8 @@ open class Wireguard : Protocol() { Log.w(TAG, "Tunnel already down") return } + statusJob?.cancel() + statusJob = null val handleToClose = tunnelHandle tunnelHandle = -1 GoBackend.awgTurnOff(handleToClose) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 193ac481..75a3f93c 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -404,7 +404,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co } apiPayload[configKey::serviceType] = serviceType; apiPayload[configKey::uuid] = installationUuid; - apiPayload[configKey::authData] = authData; + if (!authData.isEmpty()) { + apiPayload[configKey::authData] = authData; + } QSimpleCrypto::QBlockCipher blockCipher; QByteArray key = blockCipher.generatePrivateSalt(32); @@ -457,7 +459,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co auto encryptedResponseBody = reply->readAll(); - if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true)) { + if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { m_proxyUrls = getProxyUrls(); std::random_device randomDevice; std::mt19937 generator(randomDevice()); @@ -473,7 +475,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co wait.exec(); encryptedResponseBody = reply->readAll(); - if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, false)) { + if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { break; } } diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index 238d6eb0..ec4c1f8d 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -751,10 +751,6 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, DockerContainer container) { - if (credentials.userName == "root") { - return ErrorCode::NoError; - } - QString stdOut; auto cbReadStdOut = [&](const QString &data, libssh::Client &) { stdOut += data + "\n"; @@ -768,7 +764,7 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D const QString scriptData = amnezia::scriptData(SharedScriptType::check_user_in_sudo); ErrorCode error = runScript(credentials, replaceVars(scriptData, genVarsForScript(credentials)), cbReadStdOut, cbReadStdErr); - if (!stdOut.contains("sudo") && !stdOut.contains("wheel")) + if (!stdOut.contains("root :") && !stdOut.contains(" sudo") && !stdOut.contains(" wheel")) return ErrorCode::ServerUserNotInSudo; return error; @@ -832,4 +828,4 @@ ErrorCode ServerController::getDecryptedPrivateKey(const ServerCredentials &cred { auto error = m_sshClient.getDecryptedPrivateKey(credentials, decryptedPrivateKey, callback); return error; -} +} \ No newline at end of file diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 306e7f38..ae0804cb 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); - newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 3235ad0a..d2bf28ab 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -84,7 +84,7 @@ DrawerType2 { Layout.topMargin: 16 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" KeyNavigation.tab: copyConfigTextButton @@ -120,7 +120,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } @@ -143,7 +143,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 5c599013..828c32bc 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -22,9 +22,10 @@ Button { property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource - property string leftImageColor: textColor + property string leftImageColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -127,18 +128,23 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { diff --git a/client/ui/qml/Controls2/BusyIndicatorType.qml b/client/ui/qml/Controls2/BusyIndicatorType.qml index 55af280f..480f25c1 100644 --- a/client/ui/qml/Controls2/BusyIndicatorType.qml +++ b/client/ui/qml/Controls2/BusyIndicatorType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index 50f84dbf..f584a8fc 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -19,7 +19,7 @@ RadioButton { property string textColor: AmneziaStyle.color.midnightBlack - property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) + property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot property string selectedBorderColor: AmneziaStyle.color.goldenApricot property string defaultBodredColor: AmneziaStyle.color.transparent property int borderWidth: 0 diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index 6647bc88..c4b584c1 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -92,7 +92,7 @@ Item { id: background anchors.fill: parent - color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index bd4aa4fb..7a6a770e 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -24,7 +24,7 @@ Popup { Overlay.modal: Rectangle { visible: root.closeButtonVisible - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } onOpened: { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 4ec0976b..365faa94 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -183,7 +183,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom diff --git a/client/ui/qml/Controls2/TopCloseButtonType.qml b/client/ui/qml/Controls2/TopCloseButtonType.qml index 1bd7fef6..3a652da6 100644 --- a/client/ui/qml/Controls2/TopCloseButtonType.qml +++ b/client/ui/qml/Controls2/TopCloseButtonType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index c0038246..1abfbe3a 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -22,5 +22,9 @@ QtObject { readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12) readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08) readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05) + readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8) + readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3) + readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) + readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8074337a..8422a10f 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -98,7 +98,6 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.mutedGray - leftImageColor: AmneziaStyle.color.transparent borderWidth: 0 buttonTextLabel.lineHeight: 20 @@ -110,7 +109,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") - imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() @@ -166,6 +165,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -267,18 +267,39 @@ PageType { RowLayout { Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 spacing: 0 - Image { - Layout.rightMargin: 8 - visible: source !== "" - source: ServersModel.defaultServerImagePathCollapsed - } + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed + hoverEnabled: enabled + + implicitHeight: 36 + + leftPadding: 16 + rightPadding: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.transparent + textColor: AmneziaStyle.color.mutedGray + + buttonTextLabel.lineHeight: 16 + buttonTextLabel.font.pixelSize: 13 + buttonTextLabel.font.weight: 400 - LabelTextType { - id: collapsedServerMenuDescription text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } @@ -316,8 +337,8 @@ PageType { rootButtonImageColor: AmneziaStyle.color.midnightBlack rootButtonBackgroundColor: AmneziaStyle.color.paleGray - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) + rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray + rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray rootButtonHoveredBorderColor: AmneziaStyle.color.transparent rootButtonDefaultBorderColor: AmneziaStyle.color.transparent rootButtonTextTopMargin: 8 diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index f23e36d9..2d6c1d9b 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -132,8 +132,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Reload API config") @@ -172,8 +172,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Remove from application") diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 617b1091..995fa3e7 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -573,7 +573,7 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 2a565230..404ba563 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -135,7 +135,7 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/metadata/img-readme/andr.png b/metadata/img-readme/andr.png deleted file mode 100644 index a39cd52f..00000000 Binary files a/metadata/img-readme/andr.png and /dev/null differ diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png new file mode 100644 index 00000000..0e6a8850 Binary files /dev/null and b/metadata/img-readme/download.png differ diff --git a/metadata/img-readme/lin.png b/metadata/img-readme/lin.png deleted file mode 100644 index 352eae5a..00000000 Binary files a/metadata/img-readme/lin.png and /dev/null differ diff --git a/metadata/img-readme/mac.png b/metadata/img-readme/mac.png deleted file mode 100644 index 2cbb32ae..00000000 Binary files a/metadata/img-readme/mac.png and /dev/null differ diff --git a/metadata/img-readme/testiny.png b/metadata/img-readme/testiny.png new file mode 100644 index 00000000..4f38a3a9 Binary files /dev/null and b/metadata/img-readme/testiny.png differ diff --git a/metadata/img-readme/win.png b/metadata/img-readme/win.png deleted file mode 100644 index 5a35cf49..00000000 Binary files a/metadata/img-readme/win.png and /dev/null differ