mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-24 02:00:24 +07:00
merge
This commit is contained in:
@@ -14,8 +14,8 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.6.2
|
||||
QIF_VERSION: 4.7
|
||||
|
||||
steps:
|
||||
- name: 'Install Qt'
|
||||
@@ -72,8 +72,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.6.2
|
||||
QIF_VERSION: 4.7
|
||||
BUILD_ARCH: 64
|
||||
|
||||
steps:
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
runs-on: macos-13
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.2
|
||||
QT_VERSION: 6.6.2
|
||||
CC: cc
|
||||
CXX: c++
|
||||
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
env:
|
||||
# Keep compat with MacOS 10.15 aka Catalina by Qt 6.4
|
||||
QT_VERSION: 6.4.3
|
||||
QIF_VERSION: 4.6
|
||||
QIF_VERSION: 4.7
|
||||
|
||||
steps:
|
||||
- name: 'Setup xcode'
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-34
|
||||
QT_VERSION: 6.6.1
|
||||
QT_VERSION: 6.6.2
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools qtcharts'
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,6 +16,15 @@ set(PACKAGES
|
||||
Charts
|
||||
)
|
||||
|
||||
execute_process(
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMAND git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
if(IOS)
|
||||
set(PACKAGES ${PACKAGES} Multimedia)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" width="18" height="18" rx="5" fill="white"/>
|
||||
<path d="M8.49219 13.5L8.49219 9.44141L14.0191 4.99484" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.47363 5.49805L6.98828 8.0127" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.4727 9.5L14.4727 4.5033L9.50195 4.5033" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -124,7 +124,10 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
|
||||
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
|
||||
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
|
||||
// todo review wg ipv6
|
||||
#ifndef Q_OS_WINDOWS
|
||||
json.insert("deviceIpv6Address", "dead::1");
|
||||
#endif
|
||||
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
|
||||
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key));
|
||||
json.insert("serverIpv4AddrIn", wgConfig.value(amnezia::config_key::hostName));
|
||||
|
||||
@@ -225,6 +225,8 @@
|
||||
<file>images/controls/close.svg</file>
|
||||
<file>images/controls/search.svg</file>
|
||||
<file>ui/qml/Controls2/GraphViewType.qml</file>
|
||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||
<file>images/controls/split-tunneling.svg</file>
|
||||
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
+1286
-1103
File diff suppressed because it is too large
Load Diff
+1442
-1135
File diff suppressed because it is too large
Load Diff
+1363
-1105
File diff suppressed because it is too large
Load Diff
+1504
-1094
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
|
||||
m_sitesModel(sitesModel),
|
||||
m_settings(settings)
|
||||
{
|
||||
m_appVersion = QString("%1: %2 (%3)").arg(tr("Software version"), QString(APP_VERSION), __DATE__);
|
||||
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (!m_settings->isScreenshotsEnabled()) {
|
||||
|
||||
@@ -220,6 +220,11 @@ bool ServersModel::isDefaultServerCurrentlyProcessed()
|
||||
return m_defaultServerIndex == m_processedServerIndex;
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerFromApi()
|
||||
{
|
||||
return qvariant_cast<bool>(data(m_defaultServerIndex, IsServerFromApiRole));
|
||||
}
|
||||
|
||||
bool ServersModel::isProcessedServerHasWriteAccess()
|
||||
{
|
||||
return qvariant_cast<bool>(data(m_processedServerIndex, HasWriteAccessRole));
|
||||
@@ -249,7 +254,7 @@ void ServersModel::editServer(const QJsonObject &server, const int serverIndex)
|
||||
}
|
||||
updateContainersModel();
|
||||
|
||||
if (isDefaultServerCurrentlyProcessed()) {
|
||||
if (serverIndex == m_defaultServerIndex) {
|
||||
auto defaultContainer = qvariant_cast<DockerContainer>(getDefaultServerData("defaultContainer"));
|
||||
emit defaultServerDefaultContainerChanged(defaultContainer);
|
||||
}
|
||||
@@ -577,3 +582,18 @@ void ServersModel::setProcessedServerData(const QString roleString, const QVaria
|
||||
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
|
||||
{
|
||||
auto server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
|
||||
auto containerConfig = server.value(config_key::containers).toArray().at(defaultContainer).toObject();
|
||||
auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(defaultContainer)).toObject();
|
||||
|
||||
if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) {
|
||||
return !(protocolConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
} else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn || defaultContainer == DockerContainer::ShadowSocks) {
|
||||
return !(protocolConfig.value(config_key::last_config).toString().contains("redirect-gateway"));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public:
|
||||
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
|
||||
|
||||
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
|
||||
|
||||
@@ -59,6 +61,7 @@ public slots:
|
||||
const QString getDefaultServerDescriptionExpanded();
|
||||
const QString getDefaultServerDefaultContainerName();
|
||||
bool isDefaultServerCurrentlyProcessed();
|
||||
bool isDefaultServerFromApi();
|
||||
|
||||
bool isProcessedServerHasWriteAccess();
|
||||
bool isDefaultServerHasWriteAccess();
|
||||
@@ -103,6 +106,8 @@ public slots:
|
||||
QVariant getProcessedServerData(const QString roleString);
|
||||
void setProcessedServerData(const QString roleString, const QVariant &value);
|
||||
|
||||
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ void SitesModel::toggleSplitTunneling(bool enabled)
|
||||
m_settings->setRouteMode(Settings::RouteMode::VpnAllSites);
|
||||
}
|
||||
m_isSplitTunnelingEnabled = enabled;
|
||||
emit splitTunnelingToggled();
|
||||
}
|
||||
|
||||
QVector<QPair<QString, QString> > SitesModel::getCurrentSites()
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
Q_PROPERTY(int routeMode READ getRouteMode WRITE setRouteMode NOTIFY routeModeChanged)
|
||||
Q_PROPERTY(bool isTunnelingEnabled READ isSplitTunnelingEnabled NOTIFY splitTunnelingToggled)
|
||||
|
||||
public slots:
|
||||
bool addSite(const QString &hostname, const QString &ip);
|
||||
@@ -38,6 +39,7 @@ public slots:
|
||||
|
||||
signals:
|
||||
void routeModeChanged();
|
||||
void splitTunnelingToggled();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
expandedHeight: parent.height * 0.7
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
headerText: qsTr("Split tunneling")
|
||||
descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others")
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")
|
||||
|
||||
text: qsTr("Split tunneling on the server")
|
||||
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
// PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
// root.close()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
enabled: ! ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi")
|
||||
|
||||
text: qsTr("Site-based split tunneling")
|
||||
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
visible: false
|
||||
|
||||
text: qsTr("App-based split tunneling")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
// PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ Button {
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string imageSource
|
||||
property string rightImageSource
|
||||
property string leftImageColor: textColor
|
||||
|
||||
property bool squareLeftSide: false
|
||||
|
||||
@@ -118,7 +120,7 @@ Button {
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: textColor
|
||||
color: leftImageColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +133,21 @@ Button {
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredWidth: 20
|
||||
|
||||
source: root.rightImageSource
|
||||
visible: root.rightImageSource === "" ? false : true
|
||||
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ Item {
|
||||
id: emptyArea
|
||||
anchors.fill: parent
|
||||
enabled: root.isExpanded
|
||||
visible: enabled
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
|
||||
@@ -34,8 +34,45 @@ PageType {
|
||||
anchors.bottomMargin: drawer.collapsedHeight
|
||||
|
||||
ConnectButton {
|
||||
id: connectButton
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 34
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: "#878B91"
|
||||
textColor: "#878B91"
|
||||
leftImageColor: "transparent"
|
||||
borderWidth: 0
|
||||
|
||||
property bool isSplitTunnelingEnabled: SitesModel.isTunnelingEnabled ||
|
||||
(ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi"))
|
||||
|
||||
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
|
||||
|
||||
imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
||||
rightImageSource: "qrc:/images/controls/chevron-down.svg"
|
||||
|
||||
onClicked: {
|
||||
homeSplitTunnelingDrawer.open()
|
||||
}
|
||||
|
||||
HomeSplitTunnelingDrawer {
|
||||
id: homeSplitTunnelingDrawer
|
||||
|
||||
parent: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +200,7 @@ PageType {
|
||||
|
||||
LabelTextType {
|
||||
id: expandedServersMenuDescription
|
||||
Layout.bottomMargin: 24
|
||||
Layout.bottomMargin: ServersModel.isDefaultServerFromApi ? 69 : 24
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
@@ -180,6 +217,9 @@ PageType {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
spacing: 8
|
||||
|
||||
visible: !ServersModel.isDefaultServerFromApi
|
||||
onVisibleChanged: expandedServersMenuDescription.Layout
|
||||
|
||||
DropDownType {
|
||||
id: containersDropDown
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ PageType {
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
text: SettingsController.getAppVersion()
|
||||
text: qsTr("Software version: %1").arg(SettingsController.getAppVersion())
|
||||
color: "#878B91"
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,27 @@ PageType {
|
||||
visible: !GC.isMobile()
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
visible: !GC.isMobile()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
text: qsTr("Auto connect")
|
||||
descriptionText: qsTr("Connect to VPN on app start")
|
||||
|
||||
checked: SettingsController.isAutoConnectEnabled()
|
||||
onCheckedChanged: {
|
||||
if (checked !== SettingsController.isAutoConnectEnabled()) {
|
||||
SettingsController.toggleAutoConnect(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: !GC.isMobile()
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
visible: !GC.isMobile()
|
||||
|
||||
|
||||
@@ -41,27 +41,6 @@ PageType {
|
||||
headerText: qsTr("Connection")
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
visible: !GC.isMobile()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
text: qsTr("Auto connect")
|
||||
descriptionText: qsTr("Connect to VPN on app start")
|
||||
|
||||
checked: SettingsController.isAutoConnectEnabled()
|
||||
onCheckedChanged: {
|
||||
if (checked !== SettingsController.isAutoConnectEnabled()) {
|
||||
SettingsController.toggleAutoConnect(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: !GC.isMobile()
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
@@ -29,8 +29,14 @@ PageType {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isServerFromApi) {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change split tunneling settings during active connection"))
|
||||
root.pageEnabled = false
|
||||
} else if (ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && isServerFromApi) {
|
||||
PageController.showNotificationMessage(qsTr("Default server does not support split tunneling function"))
|
||||
root.pageEnabled = false
|
||||
} else {
|
||||
root.pageEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +114,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
|
||||
checked: SitesModel.isSplitTunnelingEnabled()
|
||||
checked: SitesModel.isTunnelingEnabled
|
||||
onToggled: {
|
||||
SitesModel.toggleSplitTunneling(checked)
|
||||
selector.text = root.routeModesModel[getRouteModesModelIndex()].name
|
||||
|
||||
@@ -44,6 +44,7 @@ PageType {
|
||||
|
||||
function onClosePage() {
|
||||
tabBar.isServerInfoShow = tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsServerInfo)
|
||||
&& tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsSplitTunneling)
|
||||
|
||||
if (tabBarStackView.depth <= 1) {
|
||||
return
|
||||
@@ -60,7 +61,7 @@ PageType {
|
||||
tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.Immediate)
|
||||
}
|
||||
|
||||
tabBar.isServerInfoShow = page === PageEnum.PageSettingsServerInfo || tabBar.isServerInfoShow
|
||||
tabBar.isServerInfoShow = page === PageEnum.PageSettingsServerInfo || PageEnum.PageSettingsSplitTunneling || tabBar.isServerInfoShow
|
||||
}
|
||||
|
||||
function onGoToStartPage() {
|
||||
|
||||
@@ -146,7 +146,8 @@ if [ "${MAC_CERT_PW+x}" ]; then
|
||||
fi
|
||||
|
||||
echo "Building DMG installer..."
|
||||
hdiutil create -size 120mb -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME
|
||||
# Allow Terminal to make changes in Privacy & Security > App Management
|
||||
hdiutil create -size 256mb -volname AmneziaVPN -srcfolder $BUILD_DIR/installer/$APP_NAME.app -ov -format UDZO $DMG_FILENAME
|
||||
|
||||
if [ "${MAC_CERT_PW+x}" ]; then
|
||||
echo "Signing DMG installer..."
|
||||
|
||||
Reference in New Issue
Block a user