2026-05-18 15:01:09 +03:00
import QtQuick
import QtQuick . Controls
import QtQuick . Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerProps 1.0
import ProtocolEnum 1.0
import Style 1.0
2026-06-15 19:03:15 +03:00
import TelemtConfig 1.0
2026-05-18 15:01:09 +03:00
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property int containerStatus: 1
2026-06-16 17:01:33 +03:00
// Last status-query error code (0 = none). 305 = SshTimeoutError → server unreachable.
2026-06-16 13:58:22 +03:00
property int statusErrorCode: 0
2026-05-18 15:01:09 +03:00
property bool isUpdating: false
property bool isCheckingStatus: false
2026-05-28 06:46:26 +03:00
property bool isFetchingSecret: false
2026-05-18 15:01:09 +03:00
property bool previousEnabled: true
property int previousContainerStatus: 1
property string previousPort: ""
property string previousTag: ""
property string previousPublicHost: ""
property string previousTransportMode: TelemtConfigModel . transportModeStandard ( )
property string previousTlsDomain: TelemtConfigModel . defaultTlsDomain ( )
property string previousWorkersMode: TelemtConfigModel . workersModeAuto ( )
property string previousWorkers: TelemtConfigModel . defaultWorkers ( )
property bool previousNatEnabled: false
property string previousNatInternalIp: ""
property string previousNatExternalIp: ""
2026-05-28 06:46:26 +03:00
property string previousSecret: ""
2026-05-18 15:01:09 +03:00
property string savedTransportMode: ""
property string savedTlsDomain: ""
property string savedPublicHost: ""
2026-06-15 19:03:15 +03:00
readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/
function natIpv4FieldShowInvalidError ( text ) {
var t = text ? String ( text ) . replace ( /^\s+|\s+$/g , '' ) : ""
if ( t === "" )
return false
if ( TelemtConfigModel . isValidOptionalIpv4 ( t ) )
return false
var parts = t . split ( '.' )
var j
for ( j = 0 ; j < parts . length ; j ++ ) {
if ( parts [ j ] . length > 3 )
return true
}
if ( parts . length > 4 )
return true
if ( t . indexOf ( '.' ) < 0 && t . length > 3 )
return true
if ( t . endsWith ( '.' ) )
return false
if ( parts . length < 4 )
return false
for ( var i = 0 ; i < parts . length ; i ++ ) {
if ( parts [ i ] === "" )
return true
}
return true
}
2026-05-18 15:01:09 +03:00
onSavedTransportModeChanged: {
if ( savedTransportMode === "faketls" ) {
2026-05-28 06:46:26 +03:00
root . syncedSecretTabIndex = 1
2026-05-18 15:01:09 +03:00
} else if ( savedTransportMode !== "" ) {
root . syncedSecretTabIndex = 0
}
}
property bool diagLoading: false
property int syncedSecretTabIndex: 0
property bool pendingEnableAfterRestart: false
property bool pendingUpdateAfterEnable: false
property bool diagPortReachable: false
property bool diagTelegramReachable: false
property int diagClientsConnected: - 1
property string diagLastConfigRefresh: ""
property string diagStatsEndpoint: ""
readonly property bool telemtNetworkBlocked: ! NetworkReachabilityController . hasInternetAccess
2026-05-28 06:46:26 +03:00
property bool remoteOperationBusy: false
readonly property bool operationInProgress: isCheckingStatus || isFetchingSecret || isUpdating || diagLoading
readonly property bool pageBusy: operationInProgress || remoteOperationBusy
readonly property bool navigationBlockedWhileBusy: pageBusy
property bool pageOpenHandled: false
property bool busyIndicatorShown: false
function syncPageBusyIndicator ( ) {
if ( ! root . pageOpenHandled ) {
return
}
var wantBusy = root . pageBusy
if ( wantBusy === root . busyIndicatorShown ) {
return
}
root . busyIndicatorShown = wantBusy
PageController . showBusyIndicator ( wantBusy )
}
onPageBusyChanged: syncPageBusyIndicator ( )
function telemtDomainToHex ( domain ) {
var hex = ""
for ( var i = 0 ; i < domain . length ; i ++ ) {
var code = domain . charCodeAt ( i ) . toString ( 16 )
hex += ( code . length < 2 ? "0" : "" ) + code
}
return hex
}
function telemtClientSecret ( baseHex32 , mode , tlsDomain ) {
if ( baseHex32 === "" ) {
return ""
}
if ( mode === "faketls" ) {
return "ee" + baseHex32 + telemtDomainToHex ( tlsDomain )
}
return "dd" + baseHex32
}
function telemtClientSecretForTabIndex ( baseHex32 , tabIndex , tlsDomain , defaultTlsDomain ) {
var domain = tlsDomain !== "" ? tlsDomain : defaultTlsDomain
if ( tabIndex === 1 ) {
return telemtClientSecret ( baseHex32 , "faketls" , domain )
}
return telemtClientSecret ( baseHex32 , "standard" , domain )
}
property bool containerStatusRefreshCallPending: false
function telemtRequestContainerStatusRefresh ( ) {
if ( ! NetworkReachabilityController . hasInternetAccess ) {
isCheckingStatus = false
syncPageBusyIndicator ( )
return
}
isCheckingStatus = true
syncPageBusyIndicator ( )
InstallController . refreshContainerStatus ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex )
}
function telemtScheduleContainerStatusRefresh ( ) {
if ( containerStatusRefreshCallPending ) {
return
}
containerStatusRefreshCallPending = true
Qt . callLater ( function ( ) {
containerStatusRefreshCallPending = false
root . telemtRequestContainerStatusRefresh ( )
} )
}
function telemtOnPageShown ( ) {
if ( root . pageOpenHandled ) {
return
}
root . pageOpenHandled = true
PageController . disableControls ( navigationBlockedWhileBusy )
if ( ! NetworkReachabilityController . hasInternetAccess ) {
isCheckingStatus = false
} else {
isCheckingStatus = true
}
syncPageBusyIndicator ( )
root . telemtScheduleContainerStatusRefresh ( )
}
2026-05-18 15:01:09 +03:00
function telemtScheduleUpdate ( closePage ) {
var cp = closePage === undefined ? false : closePage
Qt . callLater ( function ( ) {
2026-06-04 15:45:53 +01:00
InstallController . updateServerConfig ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex , ProtocolEnum . Telemt , cp )
2026-05-18 15:01:09 +03:00
} )
}
2026-06-16 17:01:33 +03:00
property var telemtPersistedAdditionalHex: [ ]
function telemtRefreshPersistedAdditionalSecrets ( ) {
var list = TelemtConfigModel . additionalSecretsList ( )
var a = [ ]
for ( var i = 0 ; i < list . length ; ++ i ) {
a . push ( String ( list [ i ] ) )
}
root . telemtPersistedAdditionalHex = a
}
function telemtIsPersistedAdditionalHex ( hex ) {
var h = String ( hex )
for ( var j = 0 ; j < root . telemtPersistedAdditionalHex . length ; ++ j ) {
if ( String ( root . telemtPersistedAdditionalHex [ j ] ) === h ) {
return true
}
}
return false
}
2026-05-18 15:01:09 +03:00
function statusText ( ) {
if ( isCheckingStatus ) {
return qsTr ( "Checking..." )
}
if ( isUpdating ) {
return qsTr ( "Updating" )
}
switch ( containerStatus ) {
case 0 : {
return qsTr ( "Not deployed" )
}
case 1 : {
return qsTr ( "Running" )
}
case 2 : {
return qsTr ( "Stopped" )
}
case 3 : {
return qsTr ( "Error" )
}
default: {
return qsTr ( "Unknown" )
}
}
}
Component.onCompleted: {
root . savedTransportMode = TelemtConfigModel . getTransportMode ( )
root . savedTlsDomain = TelemtConfigModel . getTlsDomain ( )
root . savedPublicHost = TelemtConfigModel . getPublicHost ( )
2026-06-16 17:01:33 +03:00
Qt . callLater ( function ( ) {
root . telemtRefreshPersistedAdditionalSecrets ( )
} )
2026-05-28 06:46:26 +03:00
Qt . callLater ( root . telemtOnPageShown )
2026-05-18 15:01:09 +03:00
}
onNavigationBlockedWhileBusyChanged: {
if ( root . visible ) {
PageController . disableControls ( navigationBlockedWhileBusy )
}
}
onVisibleChanged: {
if ( ! visible ) {
2026-05-28 06:46:26 +03:00
root . pageOpenHandled = false
containerStatusRefreshCallPending = false
isCheckingStatus = false
isFetchingSecret = false
busyIndicatorShown = false
2026-06-16 13:58:22 +03:00
statusErrorCode = 0
2026-05-18 15:01:09 +03:00
PageController . disableControls ( false )
2026-05-28 06:46:26 +03:00
PageController . showBusyIndicator ( false )
2026-05-18 15:01:09 +03:00
diagLoading = false
} else {
2026-05-28 06:46:26 +03:00
root . telemtOnPageShown ( )
2026-05-18 15:01:09 +03:00
}
}
Connections {
target: NetworkReachabilityController
function onHasInternetAccessChanged ( ) {
if ( ! root . visible ) {
return
}
if ( NetworkReachabilityController . hasInternetAccess ) {
2026-05-28 06:46:26 +03:00
root . telemtScheduleContainerStatusRefresh ( )
2026-05-18 15:01:09 +03:00
}
}
}
Connections {
target: InstallController
2026-05-28 06:46:26 +03:00
function onServerIsBusy ( busy ) {
remoteOperationBusy = busy
}
2026-05-18 15:01:09 +03:00
function onUpdateContainerFinished ( message , closePage ) {
if ( ! root . visible ) {
isUpdating = false
isCheckingStatus = false
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
2026-05-18 15:01:09 +03:00
return
}
isUpdating = false
containerStatus = 1
root . savedTransportMode = TelemtConfigModel . getTransportMode ( )
root . savedTlsDomain = TelemtConfigModel . getTlsDomain ( )
root . savedPublicHost = TelemtConfigModel . getPublicHost ( )
2026-06-16 17:01:33 +03:00
root . telemtRefreshPersistedAdditionalSecrets ( )
2026-05-18 15:01:09 +03:00
PageController . showNotificationMessage ( message )
if ( closePage ) {
PageController . closePage ( )
}
}
function onInstallationErrorOccurred ( ) {
if ( ! root . visible ) {
isUpdating = false
isCheckingStatus = false
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
2026-05-18 15:01:09 +03:00
return
}
isUpdating = false
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
2026-05-18 15:01:09 +03:00
containerStatus = previousContainerStatus
TelemtConfigModel . setEnabled ( previousEnabled )
TelemtConfigModel . setPort ( previousPort )
TelemtConfigModel . setTag ( previousTag )
TelemtConfigModel . setPublicHost ( previousPublicHost )
TelemtConfigModel . setTransportMode ( previousTransportMode )
TelemtConfigModel . setTlsDomain ( previousTlsDomain )
TelemtConfigModel . setWorkersMode ( previousWorkersMode )
TelemtConfigModel . setWorkers ( previousWorkers )
TelemtConfigModel . setNatEnabled ( previousNatEnabled )
TelemtConfigModel . setNatInternalIp ( previousNatInternalIp )
TelemtConfigModel . setNatExternalIp ( previousNatExternalIp )
2026-05-28 06:46:26 +03:00
if ( previousSecret !== "" ) {
TelemtConfigModel . setSecret ( previousSecret )
}
2026-05-18 15:01:09 +03:00
}
function onSetContainerEnabledFinished ( enabled ) {
if ( ! root . visible ) {
isUpdating = false
return
}
if ( enabled && pendingUpdateAfterEnable ) {
pendingUpdateAfterEnable = false
2026-05-28 06:46:26 +03:00
isUpdating = true
2026-05-18 15:01:09 +03:00
root . telemtScheduleUpdate ( false )
return
}
isUpdating = false
containerStatus = enabled ? 1 : 2
PageController . showNotificationMessage (
enabled ? qsTr ( "Telemt started" ) : qsTr ( "Telemt stopped" ) )
}
2026-06-16 13:58:22 +03:00
function onContainerStatusRefreshed ( status , errorCode ) {
2026-05-18 15:01:09 +03:00
if ( ! root . visible ) {
isCheckingStatus = false
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
2026-05-18 15:01:09 +03:00
return
}
containerStatus = status
2026-06-16 13:58:22 +03:00
root . statusErrorCode = errorCode
if ( status === 3 && errorCode !== 0 ) {
PageController . showNotificationMessage (
qsTr ( "Settings locked: connection timed out (error code %1). Re-open the page to retry." ) . arg ( errorCode ) )
}
2026-05-18 15:01:09 +03:00
root . savedTransportMode = TelemtConfigModel . getTransportMode ( )
root . savedTlsDomain = TelemtConfigModel . getTlsDomain ( )
root . savedPublicHost = TelemtConfigModel . getPublicHost ( )
if ( status === 1 ) {
TelemtConfigModel . setEnabled ( true )
2026-05-28 06:46:26 +03:00
isFetchingSecret = true
isCheckingStatus = false
2026-05-28 10:57:08 +08:00
InstallController . fetchContainerSecret ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex )
2026-05-28 06:46:26 +03:00
} else {
isFetchingSecret = false
isCheckingStatus = false
if ( status === 2 ) {
TelemtConfigModel . setEnabled ( false )
}
2026-05-18 15:01:09 +03:00
}
2026-05-28 06:46:26 +03:00
syncPageBusyIndicator ( )
2026-05-18 15:01:09 +03:00
}
function onContainerDiagnosticsRefreshed ( portReachable , upstreamReachable , clientsConnected , lastConfigRefresh , statsEndpoint ) {
if ( ! root . visible ) {
return
}
diagLoading = false
diagPortReachable = portReachable
diagTelegramReachable = upstreamReachable
diagClientsConnected = clientsConnected
diagLastConfigRefresh = lastConfigRefresh
diagStatsEndpoint = statsEndpoint
}
function onContainerSecretFetched ( secret ) {
if ( ! root . visible ) {
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
2026-05-18 15:01:09 +03:00
return
}
2026-05-28 06:46:26 +03:00
isFetchingSecret = false
syncPageBusyIndicator ( )
2026-05-18 15:01:09 +03:00
TelemtConfigModel . validateAndSetSecret ( secret )
}
}
2026-05-28 06:46:26 +03:00
Item {
id: contentLayer
anchors.fill: parent
enabled: ! root . pageBusy
2026-05-18 15:01:09 +03:00
BackButtonType {
id: backButton
anchors.top: parent . top
anchors.left: parent . left
anchors.right: parent . right
2026-05-28 06:46:26 +03:00
anchors.topMargin: 20 + PageController . safeAreaTopMargin
2026-05-18 15:01:09 +03:00
onFocusChanged: {
2026-05-28 06:46:26 +03:00
if ( this . activeFocus ) {
if ( mainTabBar . currentIndex === 0 ) {
connectionListView . positionViewAtBeginning ( )
} else {
settingsListView . positionViewAtBeginning ( )
}
}
2026-05-18 15:01:09 +03:00
}
}
ColumnLayout {
id: pageHeader
anchors.top: backButton . bottom
anchors.left: parent . left
anchors.right: parent . right
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
2026-05-28 06:46:26 +03:00
Layout.bottomMargin: 24
2026-05-18 15:01:09 +03:00
headerText: qsTr ( "Telemt settings" )
2026-05-28 06:46:26 +03:00
descriptionLinkText: qsTr ( "Read more about this settings" )
descriptionLinkUrl: "https://github.com/telemt/telemt"
2026-05-18 15:01:09 +03:00
}
2026-05-28 06:46:26 +03:00
CaptionTextType {
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
2026-05-28 06:46:26 +03:00
Layout.leftMargin: 16
2026-05-18 15:01:09 +03:00
Layout.rightMargin: 16
2026-05-28 06:46:26 +03:00
Layout.topMargin: 8
visible: root . telemtNetworkBlocked
text: qsTr ( "No internet connection. Connect to the internet to change Telemt settings." )
color: AmneziaStyle . color . mutedGray
wrapMode: Text . WordWrap
font.pixelSize: 14
2026-05-18 15:01:09 +03:00
}
2026-05-28 06:46:26 +03:00
}
2026-05-18 15:01:09 +03:00
2026-05-28 06:46:26 +03:00
TabBar {
id: mainTabBar
anchors.top: pageHeader . bottom
anchors.left: parent . left
anchors.right: parent . right
width: parent . width
background: Rectangle {
color: AmneziaStyle . color . transparent
Rectangle {
width: parent . width
height: 1
anchors.bottom: parent . bottom
color: AmneziaStyle . color . slateGray
2026-05-18 15:01:09 +03:00
}
2026-05-28 06:46:26 +03:00
}
2026-05-18 15:01:09 +03:00
2026-05-28 06:46:26 +03:00
TabButtonType {
text: qsTr ( "Connection" )
isSelected: mainTabBar . currentIndex === 0
}
TabButtonType {
text: qsTr ( "Settings" )
isSelected: mainTabBar . currentIndex === 1
2026-05-18 15:01:09 +03:00
}
}
StackLayout {
id: tabContent
2026-05-28 06:46:26 +03:00
anchors.top: mainTabBar . bottom
2026-05-18 15:01:09 +03:00
anchors.bottom: parent . bottom
anchors.left: parent . left
anchors.right: parent . right
currentIndex: mainTabBar . currentIndex
ListViewType {
id: connectionListView
model: TelemtConfigModel
delegate: ColumnLayout {
width: connectionListView . width
spacing: 0
property int secretTabIndex: root . syncedSecretTabIndex
function activeSecret ( ) {
2026-05-28 06:46:26 +03:00
return root . telemtClientSecretForTabIndex ( secret , root . syncedSecretTabIndex ,
root . savedTlsDomain , TelemtConfigModel . defaultTlsDomain ( ) )
2026-05-18 15:01:09 +03:00
}
function effectiveSecret ( ) {
return activeSecret ( )
}
function effectiveHost ( ) {
2026-05-28 10:57:08 +08:00
return root . savedPublicHost !== "" ? root.savedPublicHost : ServersUiController . serverHostName ( ServersUiController . processedServerId )
2026-05-18 15:01:09 +03:00
}
function tmeLink ( ) {
return "https://t.me/proxy?server=" + effectiveHost ( ) + "&port=" + port + "&secret=" + activeSecret ( )
}
function tgLink ( ) {
return "tg://proxy?server=" + effectiveHost ( ) + "&port=" + port + "&secret=" + activeSecret ( )
}
CaptionTextType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
text: qsTr ( "Use Telegram connection link" )
color: AmneziaStyle . color . mutedGray
}
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
implicitHeight: linkRow . implicitHeight + 16
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
RowLayout {
id: linkRow
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 4
CaptionTextType {
Layout.fillWidth: true
text: secret !== "" ? tmeLink ( ) : qsTr ( "Deploy Telemt first" )
color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle . color . mutedGray
elide: Text . ElideRight
maximumLineCount: 1
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle . color . paleGray
visible: secret !== ""
onClicked: {
ExportController . generateQrFromString ( tmeLink ( ) )
PageController . goToShareConnectionPage (
qsTr ( "Telegram connection link" ) ,
qsTr ( "Telemt connection link" ) ,
"" , "" , "" )
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
visible: secret !== ""
onClicked: {
GC . copyToClipBoard ( tmeLink ( ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) )
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
implicitHeight: tgLinkRow . implicitHeight + 16
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
visible: secret !== ""
RowLayout {
id: tgLinkRow
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 4
CaptionTextType {
Layout.fillWidth: true
text: tgLink ( )
color: AmneziaStyle . color . goldenApricot
elide: Text . ElideRight
maximumLineCount: 1
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
ExportController . generateQrFromString ( tgLink ( ) )
PageController . goToShareConnectionPage (
qsTr ( "Telegram connection link" ) ,
qsTr ( "Telemt connection link" ) ,
"" , "" , "" )
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
GC . copyToClipBoard ( tgLink ( ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) )
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
spacing: 4
CaptionTextType {
text: qsTr ( "Or enter the proxy details manually." )
color: AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "How to do it" )
color: AmneziaStyle . color . goldenApricot
MouseArea {
anchors.fill: parent
cursorShape: Qt . PointingHandCursor
onClicked: Qt . openUrlExternally ( "https://core.telegram.org/proxy" )
}
}
Item {
Layout.fillWidth: true
}
}
Rectangle {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
implicitHeight: manualCol . implicitHeight + 8
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
ColumnLayout {
id: manualCol
anchors.left: parent . left
anchors.right: parent . right
anchors.top: parent . top
anchors.topMargin: 8
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.rightMargin: 8
Layout.bottomMargin: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CaptionTextType {
text: qsTr ( "Host" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
text: effectiveHost ( )
color: AmneziaStyle . color . paleGray
elide: Text . ElideRight
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: { GC . copyToClipBoard ( effectiveHost ( ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) ) }
}
}
DividerType {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.rightMargin: 8
Layout.topMargin: 8
Layout.bottomMargin: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CaptionTextType {
text: qsTr ( "Port" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
text: port
color: AmneziaStyle . color . paleGray
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: { GC . copyToClipBoard ( port )
PageController . showNotificationMessage ( qsTr ( "Copied" ) ) }
}
}
DividerType {
Layout.fillWidth: true
}
ButtonGroup {
id: secretTabGroup
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.rightMargin: 8
Layout.topMargin: 4
Layout.bottomMargin: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CaptionTextType {
text: qsTr ( "Secret" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
text: activeSecret ( )
color: AmneziaStyle . color . paleGray
wrapMode: Text . WrapAnywhere
font.pixelSize: 13
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: { GC . copyToClipBoard ( activeSecret ( ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) ) }
}
}
}
}
LabelWithButtonType {
id: removeButton
Layout.fillWidth: true
Layout.bottomMargin: 24
Layout.leftMargin: 0
Layout.rightMargin: 16
2026-05-28 10:57:08 +08:00
visible: ServersUiController . isProcessedServerHasWriteAccess ( )
2026-05-18 15:01:09 +03:00
text: qsTr ( "Delete Telemt" )
textColor: AmneziaStyle . color . vibrantRed
clickedFunction: function ( ) {
var headerText = qsTr ( "Remove %1 from server?" ) . arg ( ContainersModel . getProcessedContainerName ( ) )
var descriptionText = qsTr ( "The proxy will be stopped and all users will lose access." )
var yesButtonText = qsTr ( "Continue" )
var noButtonText = qsTr ( "Cancel" )
var yesButtonFunction = function ( ) {
PageController . goToPage ( PageEnum . PageDeinstalling )
2026-05-28 10:57:08 +08:00
InstallController . removeContainer ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex )
2026-05-18 15:01:09 +03:00
}
showQuestionDrawer ( headerText , descriptionText , yesButtonText , noButtonText , yesButtonFunction , function ( ) {
} )
}
MouseArea {
anchors.fill: removeButton
cursorShape: Qt . PointingHandCursor
enabled: false
}
}
}
}
ListViewType {
id: settingsListView
model: TelemtConfigModel
reuseItems: false
delegate: ColumnLayout {
2026-06-16 17:01:33 +03:00
id: settingsRoot
2026-05-18 15:01:09 +03:00
width: settingsListView . width
spacing: 0
2026-06-16 13:58:22 +03:00
readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && ! root . pageBusy
2026-05-28 06:46:26 +03:00
function telemtActiveSecretForBaseHex ( baseHex ) {
return root . telemtClientSecretForTabIndex ( baseHex , root . syncedSecretTabIndex ,
root . savedTlsDomain , TelemtConfigModel . defaultTlsDomain ( ) )
}
2026-06-16 17:01:33 +03:00
function telemtEffectiveHostForLinks ( ) {
return root . savedPublicHost !== "" ? root.savedPublicHost : ServersUiController . serverHostName ( ServersUiController . processedServerId )
}
function telemtTmeLinkForAdditional ( baseHex ) {
return "https://t.me/proxy?server=" + telemtEffectiveHostForLinks ( ) + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex ( baseHex )
}
function telemtTgLinkForAdditional ( baseHex ) {
return "tg://proxy?server=" + telemtEffectiveHostForLinks ( ) + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex ( baseHex )
}
2026-05-18 15:01:09 +03:00
SwitcherType {
id: enableTelemtSwitch
Layout.fillWidth: true
Layout.topMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr ( "Enable Telemt" )
checked: isEnabled
2026-05-28 06:46:26 +03:00
enabled: containerStatus !== 0 && containerStatus !== 3 && ! root . pageBusy
&& ! root . telemtNetworkBlocked
2026-05-18 15:01:09 +03:00
onToggled: function ( ) {
if ( checked !== isEnabled ) {
previousEnabled = isEnabled
previousContainerStatus = containerStatus
2026-05-28 06:46:26 +03:00
root . previousSecret = secret
2026-05-18 15:01:09 +03:00
isEnabled = checked
isUpdating = true
if ( checked ) {
root . pendingUpdateAfterEnable = true
2026-05-28 10:57:08 +08:00
InstallController . setContainerEnabled ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex , true )
2026-05-18 15:01:09 +03:00
} else {
2026-05-28 10:57:08 +08:00
InstallController . setContainerEnabled ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex , false )
2026-05-18 15:01:09 +03:00
}
}
}
}
2026-06-16 13:58:22 +03:00
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
visible: ! fieldsEditable && ! root . pageBusy
text: ( containerStatus === 1 || containerStatus === 2 )
? qsTr ( "Enable Telemt to edit settings" )
: ( statusErrorCode !== 0
? qsTr ( "Settings locked: connection timed out (error code %1). Re-open the page to retry." ) . arg ( statusErrorCode )
: qsTr ( "Cannot reach the server — settings are unavailable" ) )
color: AmneziaStyle . color . mutedGray
wrapMode: Text . WordWrap
}
2026-05-18 15:01:09 +03:00
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16 * 2
spacing: 4
CaptionTextType {
text: qsTr ( "Base secret" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
RowLayout {
Layout.fillWidth: true
spacing: 8
CaptionTextType {
Layout.fillWidth: true
2026-05-28 06:46:26 +03:00
text: secret !== "" ? telemtActiveSecretForBaseHex ( secret ) : qsTr ( "Not generated" )
2026-05-18 15:01:09 +03:00
color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle . color . mutedGray
2026-05-28 06:46:26 +03:00
wrapMode: Text . WrapAnywhere
2026-05-18 15:01:09 +03:00
font.pixelSize: 14
}
ImageButtonType {
2026-05-28 06:46:26 +03:00
Layout.alignment: Qt . AlignTop
2026-05-18 15:01:09 +03:00
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/refresh-cw.svg"
imageColor: AmneziaStyle . color . paleGray
2026-05-28 10:57:08 +08:00
visible: ServersUiController . isProcessedServerHasWriteAccess ( )
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
onClicked: {
2026-05-28 06:46:26 +03:00
var secretSnapshot = secret
2026-05-18 15:01:09 +03:00
showQuestionDrawer (
qsTr ( "Generate new secret?" ) ,
qsTr ( "All existing connection links will stop working. Users will need new links." ) ,
qsTr ( "Generate" ) ,
qsTr ( "Cancel" ) ,
function ( ) {
2026-05-28 06:46:26 +03:00
root . previousSecret = secretSnapshot
2026-05-18 15:01:09 +03:00
if ( containerStatus === 1 ) {
isUpdating = true
TelemtConfigModel . generateSecret ( )
root . telemtScheduleUpdate ( false )
} else {
TelemtConfigModel . generateSecret ( )
PageController . showNotificationMessage ( qsTr ( "New secret saved. It will be applied when Telemt is started." ) )
}
} ,
function ( ) {
}
)
}
}
}
}
TextFieldWithHeaderType {
id: publicHostTextField
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 4
headerText: qsTr ( "Public host / IP" )
2026-05-28 10:57:08 +08:00
textField.placeholderText: ServersUiController . serverHostName ( ServersUiController . processedServerId )
2026-05-18 15:01:09 +03:00
textField.text: publicHost
2026-06-15 19:03:15 +03:00
textField.maximumLength: 253
textField.validator: PublicHostInputValidator {
}
textField.onTextChanged: {
var t = publicHostTextField . textField . text
if ( TelemtConfigModel . isPublicHostTypingIncomplete ( t ) ) {
publicHostTextField . errorText = ""
} else if ( ! TelemtConfigModel . isValidPublicHost ( t ) ) {
publicHostTextField . errorText = qsTr ( "Enter a valid IP address or domain name" )
} else {
publicHostTextField . errorText = ""
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
textField . text = textField . text . replace ( /^\s+|\s+$/g , '' )
2026-06-15 19:03:15 +03:00
if ( ! TelemtConfigModel . isValidPublicHost ( textField . text ) ) {
publicHostTextField . errorText = qsTr ( "Enter a valid IP address or domain name" )
return
}
publicHostTextField . errorText = ""
2026-05-18 15:01:09 +03:00
if ( textField . text !== publicHost ) {
publicHost = textField . text
TelemtConfigModel . setPublicHost ( publicHost )
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 4
visible: publicHostTextField . textField . text === ""
text: qsTr ( "Leave empty to use server IP automatically" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
wrapMode: Text . WordWrap
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 12
visible: publicHostTextField . textField . text !== "" &&
2026-05-28 10:57:08 +08:00
publicHostTextField . textField . text !== ServersUiController . serverHostName ( ServersUiController . processedServerId )
2026-05-18 15:01:09 +03:00
text: qsTr ( "⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server." )
color: AmneziaStyle . color . goldenApricot
font.pixelSize: 12
wrapMode: Text . WordWrap
}
TextFieldWithHeaderType {
id: portTextField
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
headerText: qsTr ( "Server port" )
textField.placeholderText: TelemtConfigModel . defaultPort ( )
textField.maximumLength: 5
2026-06-15 19:03:15 +03:00
textField.inputMethodHints: Qt . ImhDigitsOnly
2026-05-18 15:01:09 +03:00
textField.validator: IntValidator {
bottom: 1
top: 65535
}
Component.onCompleted: {
var savedPort = port
textField . text = ( savedPort === TelemtConfigModel . defaultPort ( ) ) ? "" : savedPort
}
2026-06-15 19:03:15 +03:00
textField.onTextChanged: {
var cur = portTextField . textField . text
var clean = TelemtConfigModel . sanitizePortFieldText ( cur )
if ( clean !== cur ) {
textField . text = clean
textField . cursorPosition = clean . length
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
2026-06-15 19:03:15 +03:00
textField . text = TelemtConfigModel . sanitizePortFieldText ( textField . text )
2026-05-18 15:01:09 +03:00
var portValue = textField . text === "" ? TelemtConfigModel . defaultPort ( ) : textField . text
if ( portValue !== port ) {
port = portValue
TelemtConfigModel . setPort ( port )
}
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 12
visible: transportMode === "faketls" && portTextField . textField . text !== "443" && portTextField . textField . text !== ""
text: qsTr ( "FakeTLS may not work on ports other than 443" )
color: AmneziaStyle . color . goldenApricot
font.pixelSize: 12
wrapMode: Text . WordWrap
}
2026-06-16 17:01:33 +03:00
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
text: qsTr ( "The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty." )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
wrapMode: Text . WordWrap
}
2026-05-18 15:01:09 +03:00
TextFieldWithHeaderType {
id: tagTextField
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
headerText: qsTr ( "Promoted channel tag (optional)" )
2026-06-15 19:03:15 +03:00
textField.placeholderText: qsTr ( "32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)" )
2026-05-18 15:01:09 +03:00
textField.text: tag
2026-06-15 19:03:15 +03:00
textField.maximumLength: TelemtConfigModel . mtProxyBotTagHexLength ( )
textField.onTextChanged: {
var cur = tagTextField . textField . text
var clean = TelemtConfigModel . sanitizeMtProxyTagFieldText ( cur )
if ( clean !== cur ) {
textField . text = clean
textField . cursorPosition = clean . length
return
}
var tt = tagTextField . textField . text
if ( tt === "" ) {
tagTextField . errorText = ""
return
}
if ( TelemtConfigModel . isMtProxyTagTypingIncomplete ( tt ) ) {
tagTextField . errorText = ""
return
}
if ( ! TelemtConfigModel . isValidMtProxyTag ( tt ) ) {
tagTextField . errorText = qsTr ( "Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F)." )
return
}
tagTextField . errorText = ""
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
2026-06-15 19:03:15 +03:00
var raw = textField . text . replace ( /^\s+|\s+$/g , '' )
var normalized = TelemtConfigModel . sanitizeMtProxyTagFieldText ( raw )
textField . text = normalized
if ( ! TelemtConfigModel . isValidMtProxyTag ( normalized ) ) {
tagTextField . errorText = qsTr ( "Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused." )
return
}
tagTextField . errorText = ""
if ( normalized !== tag ) {
tag = normalized
2026-05-18 15:01:09 +03:00
TelemtConfigModel . setTag ( tag )
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
spacing: 4
CaptionTextType {
text: qsTr ( "Get a tag from" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
CaptionTextType {
text: "@MTProxyBot"
color: AmneziaStyle . color . goldenApricot
font.pixelSize: 12
MouseArea {
anchors.fill: parent
cursorShape: Qt . PointingHandCursor
onClicked: Qt . openUrlExternally ( "https://t.me/MTProxyBot" )
}
}
}
2026-06-15 17:32:17 +03:00
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16 * 2
text: qsTr ( "Transport mode" )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
}
2026-05-18 15:01:09 +03:00
DropDownType {
id: transportModeDropDown
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
drawerParent: root
drawerHeight: 0.35
2026-06-15 17:32:17 +03:00
headerText: qsTr ( "Transport mode" )
2026-05-18 15:01:09 +03:00
text: transportMode === "faketls" ? qsTr ( "FakeTLS" ) : qsTr ( "Standard MTProto" )
listView: Component {
ListViewType {
model: [ qsTr ( "Standard MTProto" ) , qsTr ( "FakeTLS" ) ]
delegate: LabelWithButtonType {
Layout.fillWidth: true
text: modelData
rightImageSource: {
var isCurrent = ( index === 0 && transportMode === "standard" ) ||
( index === 1 && transportMode === "faketls" )
return isCurrent ? "qrc:/images/controls/check.svg" : ""
}
rightImageColor: AmneziaStyle . color . goldenApricot
clickedFunction: function ( ) {
transportMode = ( index === 0 ) ? "standard" : "faketls"
TelemtConfigModel . setTransportMode ( transportMode )
2026-05-28 06:46:26 +03:00
root . syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0
2026-05-18 15:01:09 +03:00
transportModeDropDown . closeTriggered ( )
}
}
}
}
}
TextFieldWithHeaderType {
id: tlsDomainTextField
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: transportMode === "faketls"
headerText: qsTr ( "FakeTLS domain" )
textField.placeholderText: root . previousTlsDomain
2026-06-15 19:03:15 +03:00
textField.validator: RegularExpressionValidator {
regularExpression: /^[A-Za-z0-9.-]*$/
}
2026-05-18 15:01:09 +03:00
Component.onCompleted: {
var savedDomain = tlsDomain
textField . text = ( savedDomain === TelemtConfigModel . defaultTlsDomain ( ) || savedDomain === "" ) ? "" : savedDomain
}
2026-06-15 19:03:15 +03:00
textField.onTextChanged: {
var t = tlsDomainTextField . textField . text
if ( t === "" || TelemtConfigModel . isFakeTlsDomainTypingIncomplete ( t )
|| TelemtConfigModel . isValidFakeTlsDomain ( t ) ) {
tlsDomainTextField . errorText = ""
} else {
tlsDomainTextField . errorText = qsTr ( "Enter a valid domain name" )
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
textField . text = textField . text . replace ( /^\s+|\s+$/g , '' )
var domainValue = textField . text === "" ? TelemtConfigModel . defaultTlsDomain ( ) : textField . text
2026-06-15 19:03:15 +03:00
if ( ! TelemtConfigModel . isValidFakeTlsDomain ( domainValue ) ) {
tlsDomainTextField . errorText = qsTr ( "Enter a valid domain name" )
return
}
tlsDomainTextField . errorText = ""
2026-05-18 15:01:09 +03:00
if ( domainValue !== tlsDomain ) {
tlsDomain = domainValue
TelemtConfigModel . setTlsDomain ( tlsDomain )
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
spacing: 4
visible: transportMode === "faketls"
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3." )
color: AmneziaStyle . color . mutedGray
wrapMode: Text . WordWrap
font.pixelSize: 12
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links." )
color: AmneziaStyle . color . goldenApricot
wrapMode: Text . WordWrap
font.pixelSize: 12
}
}
LabelWithButtonType {
id: advancedHeader
Layout.fillWidth: true
Layout.leftMargin: 0
Layout.rightMargin: 16
property bool expanded: false
text: qsTr ( "Advanced" )
rightImageSource: expanded
? "qrc:/images/controls/chevron-up.svg"
: "qrc:/images/controls/chevron-down.svg"
rightImageColor: AmneziaStyle . color . mutedGray
clickedFunction: function ( ) {
expanded = ! expanded
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
visible: advancedHeader . expanded
2026-06-16 13:58:22 +03:00
enabled: fieldsEditable
2026-05-18 15:01:09 +03:00
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 4
text: qsTr ( "Additional secrets" )
color: AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
text: qsTr ( "Add extra secrets to allow gradual migration without disconnecting existing users." )
color: AmneziaStyle . color . charcoalGray
wrapMode: Text . WordWrap
font.pixelSize: 12
}
Repeater {
model: additionalSecrets
2026-06-16 17:01:33 +03:00
delegate: ColumnLayout {
id: addSecretDelegate
property bool linksExpanded: false
readonly property bool linksPanelAllowed: root . telemtIsPersistedAdditionalHex ( modelData )
2026-05-18 15:01:09 +03:00
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
2026-06-16 17:01:33 +03:00
Layout.bottomMargin: 8
spacing: 0
onLinksPanelAllowedChanged: {
if ( ! linksPanelAllowed ) {
linksExpanded = false
}
2026-05-18 15:01:09 +03:00
}
2026-06-16 17:01:33 +03:00
Rectangle {
Layout.fillWidth: true
implicitHeight: collapsedBar . implicitHeight + 16
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
RowLayout {
id: collapsedBar
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 8
Item {
Layout.fillWidth: true
implicitHeight: Math . max ( hexCaption . implicitHeight , 24 )
RowLayout {
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
spacing: 8
CaptionTextType {
id: hexCaption
Layout.fillWidth: true
text: modelData
color: AmneziaStyle . color . paleGray
elide: Text . ElideMiddle
font.pixelSize: 13
}
Image {
width: 24
height: 24
visible: addSecretDelegate . linksPanelAllowed
source: "qrc:/images/controls/chevron-down.svg"
sourceSize.width: 24
sourceSize.height: 24
rotation: addSecretDelegate . linksExpanded ? 180 : 0
Behavior on rotation {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
anchors.fill: parent
visible: addSecretDelegate . linksPanelAllowed
enabled: addSecretDelegate . linksPanelAllowed
cursorShape: Qt . PointingHandCursor
onClicked: addSecretDelegate . linksExpanded = ! addSecretDelegate . linksExpanded
}
}
ImageButtonType {
implicitWidth: 32
implicitHeight: 32
hoverEnabled: true
visible: ServersUiController . isProcessedServerHasWriteAccess ( )
image: "qrc:/images/controls/trash.svg"
imageColor: AmneziaStyle . color . vibrantRed
onClicked: {
TelemtConfigModel . removeAdditionalSecret ( index )
if ( containerStatus === 1 ) {
root . telemtScheduleUpdate ( false )
}
}
}
}
2026-05-18 15:01:09 +03:00
}
2026-06-16 17:01:33 +03:00
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 8
spacing: 8
visible: addSecretDelegate . linksPanelAllowed && addSecretDelegate . linksExpanded
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "Use Telegram connection link" )
color: AmneziaStyle . color . mutedGray
}
Rectangle {
Layout.fillWidth: true
implicitHeight: expTmeRow . implicitHeight + 16
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
RowLayout {
id: expTmeRow
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 4
CaptionTextType {
Layout.fillWidth: true
text: settingsRoot . telemtTmeLinkForAdditional ( modelData )
color: AmneziaStyle . color . goldenApricot
elide: Text . ElideRight
maximumLineCount: 1
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
ExportController . generateQrFromString ( settingsRoot . telemtTmeLinkForAdditional ( modelData ) )
PageController . goToShareConnectionPage (
qsTr ( "Telegram connection link" ) ,
qsTr ( "Telemt connection link" ) ,
"" , "" , "" )
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
GC . copyToClipBoard ( settingsRoot . telemtTmeLinkForAdditional ( modelData ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) )
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: expTgRow . implicitHeight + 16
color: AmneziaStyle . color . onyxBlack
radius: 8
border.color: AmneziaStyle . color . slateGray
border.width: 1
RowLayout {
id: expTgRow
anchors.left: parent . left
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 8
spacing: 4
CaptionTextType {
Layout.fillWidth: true
text: settingsRoot . telemtTgLinkForAdditional ( modelData )
color: AmneziaStyle . color . goldenApricot
elide: Text . ElideRight
maximumLineCount: 1
font.pixelSize: 13
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/qr-code.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
ExportController . generateQrFromString ( settingsRoot . telemtTgLinkForAdditional ( modelData ) )
PageController . goToShareConnectionPage (
qsTr ( "Telegram connection link" ) ,
qsTr ( "Telemt connection link" ) ,
"" , "" , "" )
}
}
ImageButtonType {
implicitWidth: 36
implicitHeight: 36
hoverEnabled: true
image: "qrc:/images/controls/copy.svg"
imageColor: AmneziaStyle . color . paleGray
onClicked: {
GC . copyToClipBoard ( settingsRoot . telemtTgLinkForAdditional ( modelData ) )
PageController . showNotificationMessage ( qsTr ( "Copied" ) )
}
}
}
2026-05-18 15:01:09 +03:00
}
}
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
text: qsTr ( "Add additional secret" )
clickedFunc: function ( ) {
TelemtConfigModel . addAdditionalSecret ( )
}
}
DividerType {
Layout.fillWidth: true
Layout.bottomMargin: 8
}
LabelTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.bottomMargin: 4
text: qsTr ( "Worker mode" )
}
ButtonGroup {
id: workerModeGroup
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 4
spacing: 0
visible: transportMode !== "faketls"
HorizontalRadioButton {
Layout.fillWidth: true
text: qsTr ( "Auto" )
ButtonGroup.group: workerModeGroup
checked: workersMode === "auto"
onClicked: { workersMode = "auto" ; TelemtConfigModel . setWorkersMode ( "auto" ) }
}
HorizontalRadioButton {
Layout.fillWidth: true
text: qsTr ( "Manual" )
ButtonGroup.group: workerModeGroup
checked: workersMode === "manual"
onClicked: { workersMode = "manual" ; TelemtConfigModel . setWorkersMode ( "manual" ) }
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
visible: transportMode === "faketls"
text: qsTr ( "Workers are set to 0 automatically for FakeTLS mode." )
color: AmneziaStyle . color . mutedGray
font.pixelSize: 12
wrapMode: Text . WordWrap
}
TextFieldWithHeaderType {
id: workersTextField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: workersMode === "manual" && transportMode !== "faketls"
headerText: qsTr ( "Workers count" )
textField.placeholderText: "2"
textField.text: workers
2026-06-15 18:23:55 +03:00
textField.maximumLength: 2
textField.inputMethodHints: Qt . ImhDigitsOnly
// Range input like the port field: IntValidator bounds the value and the
// clamp keeps it within 0..maxWorkers on every change (rejects 33+, neg.).
2026-05-18 15:01:09 +03:00
textField.validator: IntValidator {
2026-06-15 18:23:55 +03:00
bottom: 0
2026-05-18 15:01:09 +03:00
top: TelemtConfigModel . maxWorkers ( )
}
2026-06-15 18:23:55 +03:00
textField.onTextChanged: {
var cur = workersTextField . textField . text
if ( cur === "" ) {
return
}
var n = parseInt ( cur , 10 )
var maxW = TelemtConfigModel . maxWorkers ( )
if ( isNaN ( n ) || n < 0 ) { n = 0 }
if ( n > maxW ) { n = maxW }
var clamped = String ( n )
if ( clamped !== cur ) {
textField . text = clamped
textField . cursorPosition = clamped . length
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
2026-06-15 18:23:55 +03:00
var v = workersTextField . textField . text
if ( v !== "" ) {
var m = parseInt ( v , 10 )
var maxW2 = TelemtConfigModel . maxWorkers ( )
if ( isNaN ( m ) || m < 0 ) { m = 0 }
if ( m > maxW2 ) { m = maxW2 }
v = String ( m )
textField . text = v
}
if ( v !== workers ) {
workers = v
2026-05-18 15:01:09 +03:00
TelemtConfigModel . setWorkers ( workers )
}
}
}
DividerType {
Layout.fillWidth: true
Layout.bottomMargin: 8
}
SwitcherType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 4
text: qsTr ( "Server is behind NAT / Docker bridge" )
descriptionText: qsTr ( "Enable if your server is not directly accessible from the internet, e.g. Docker or private network" )
checked: natEnabled
onToggled: function ( ) {
if ( checked !== natEnabled ) {
natEnabled = checked
TelemtConfigModel . setNatEnabled ( natEnabled )
}
}
}
TextFieldWithHeaderType {
id: natInternalIpTextField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: natEnabled
headerText: qsTr ( "Internal IP" )
textField.placeholderText: "172.17.0.2"
textField.text: natInternalIp
2026-06-15 19:03:15 +03:00
textField.maximumLength: 15
textField.validator: RegularExpressionValidator {
regularExpression: root . natIpv4InputFormat
}
textField.onTextChanged: {
if ( root . natIpv4FieldShowInvalidError ( textField . text ) ) {
natInternalIpTextField . errorText = qsTr ( "Enter a valid IPv4 address" )
} else {
natInternalIpTextField . errorText = ""
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
textField . text = textField . text . replace ( /^\s+|\s+$/g , '' )
2026-06-15 19:03:15 +03:00
if ( ! TelemtConfigModel . isValidOptionalIpv4 ( textField . text ) ) {
natInternalIpTextField . errorText = qsTr ( "Enter a valid IPv4 address" )
return
}
natInternalIpTextField . errorText = ""
2026-05-18 15:01:09 +03:00
if ( textField . text !== natInternalIp ) {
natInternalIp = textField . text
TelemtConfigModel . setNatInternalIp ( natInternalIp )
}
}
}
TextFieldWithHeaderType {
id: natExternalIpTextField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: natEnabled
headerText: qsTr ( "External IP" )
textField.placeholderText: "1.2.3.4"
textField.text: natExternalIp
2026-06-15 19:03:15 +03:00
textField.maximumLength: 15
textField.validator: RegularExpressionValidator {
regularExpression: root . natIpv4InputFormat
}
textField.onTextChanged: {
if ( root . natIpv4FieldShowInvalidError ( textField . text ) ) {
natExternalIpTextField . errorText = qsTr ( "Enter a valid IPv4 address" )
} else {
natExternalIpTextField . errorText = ""
}
}
2026-05-18 15:01:09 +03:00
textField.onEditingFinished: {
textField . text = textField . text . replace ( /^\s+|\s+$/g , '' )
2026-06-15 19:03:15 +03:00
if ( ! TelemtConfigModel . isValidOptionalIpv4 ( textField . text ) ) {
natExternalIpTextField . errorText = qsTr ( "Enter a valid IPv4 address" )
return
}
natExternalIpTextField . errorText = ""
2026-05-18 15:01:09 +03:00
if ( textField . text !== natExternalIp ) {
natExternalIp = textField . text
TelemtConfigModel . setNatExternalIp ( natExternalIp )
}
}
}
}
DividerType {
Layout.fillWidth: true
Layout.topMargin: 8
}
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 8
spacing: 8
visible: containerStatus === 1
RowLayout {
Layout.fillWidth: true
Header2Type {
Layout.fillWidth: true
headerText: qsTr ( "Diagnostics" )
}
ImageButtonType {
implicitWidth: 32
implicitHeight: 32
image: "qrc:/images/controls/refresh-cw.svg"
imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle . color . paleGray
hoverEnabled: ! diagLoading
enabled: ! diagLoading
onClicked: {
diagLoading = true
2026-05-28 10:57:08 +08:00
InstallController . refreshContainerDiagnostics ( ServersUiController . processedServerId , ServersUiController . processedContainerIndex , parseInt ( port ) )
2026-05-18 15:01:09 +03:00
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
width: 8
height: 8
radius: 4
color: diagClientsConnected >= 0 ? ( diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle . color . vibrantRed ) : AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "Public port reachable" )
color: AmneziaStyle . color . paleGray
}
CaptionTextType {
text: diagClientsConnected < 0 ? qsTr ( "—" ) : ( diagPortReachable ? qsTr ( "Yes" ) : qsTr ( "No" ) )
color: diagClientsConnected >= 0 ? ( diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle . color . vibrantRed ) : AmneziaStyle . color . mutedGray
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
width: 8
height: 8
radius: 4
color: diagClientsConnected >= 0 ? ( diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle . color . vibrantRed ) : AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "Telegram upstream reachable" )
color: AmneziaStyle . color . paleGray
}
CaptionTextType {
text: diagClientsConnected < 0 ? qsTr ( "—" ) : ( diagTelegramReachable ? qsTr ( "Yes" ) : qsTr ( "No" ) )
color: diagClientsConnected >= 0 ? ( diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle . color . vibrantRed ) : AmneziaStyle . color . mutedGray
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
width: 8
height: 8
radius: 4
color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "Clients connected" )
color: AmneziaStyle . color . paleGray
}
CaptionTextType {
text: diagClientsConnected < 0 ? qsTr ( "—" ) : diagClientsConnected . toString ( )
color: AmneziaStyle . color . paleGray
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
Rectangle {
width: 8
height: 8
radius: 4
color: AmneziaStyle . color . mutedGray
}
CaptionTextType {
Layout.fillWidth: true
text: qsTr ( "Last config refresh" )
color: AmneziaStyle . color . paleGray
}
CaptionTextType {
text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr ( "—" )
color: AmneziaStyle . color . mutedGray
}
}
LabelWithButtonType {
Layout.fillWidth: true
Layout.leftMargin: - 16
visible: diagStatsEndpoint !== ""
text: qsTr ( "Stats endpoint" )
descriptionText: diagStatsEndpoint
descriptionOnTop: true
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: AmneziaStyle . color . paleGray
clickedFunction: function ( ) {
GC . copyToClipBoard ( diagStatsEndpoint )
PageController . showNotificationMessage ( qsTr ( "Copied" ) )
}
}
CaptionTextType {
Layout.fillWidth: true
text: diagLoading ? qsTr ( "Refreshing…" ) : qsTr ( "Tap ↻ to refresh diagnostics" )
color: AmneziaStyle . color . mutedGray
visible: diagClientsConnected < 0
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16 * 2
Layout.bottomMargin: 24
text: qsTr ( "If you change the settings, the proxy connection link will change. The old link will stop working." )
color: AmneziaStyle . color . mutedGray
wrapMode: Text . WordWrap
font.pixelSize: 12
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.bottomMargin: 32
Layout.rightMargin: 16
Layout.leftMargin: 16
2026-05-28 10:57:08 +08:00
visible: ServersUiController . isProcessedServerHasWriteAccess ( )
2026-06-16 17:01:33 +03:00
enabled: fieldsEditable && ! root . telemtNetworkBlocked
2026-05-18 15:01:09 +03:00
text: qsTr ( "Save" )
clickedFunc: function ( ) {
2026-06-16 17:01:33 +03:00
if ( root . telemtNetworkBlocked ) {
PageController . showErrorMessage ( qsTr ( "No internet connection. Connect to the internet to change Telemt settings." ) )
return
}
publicHostTextField . errorText = ""
tagTextField . errorText = ""
tlsDomainTextField . errorText = ""
natInternalIpTextField . errorText = ""
natExternalIpTextField . errorText = ""
portTextField . errorText = ""
2026-05-18 15:01:09 +03:00
var portValue = portTextField . textField . text === ""
? TelemtConfigModel . defaultPort ( )
: portTextField . textField . text
2026-06-16 17:01:33 +03:00
var errorLines = [ ]
var bullet = "- "
2026-05-18 15:01:09 +03:00
if ( ! portTextField . textField . acceptableInput && portTextField . textField . text !== "" ) {
2026-06-16 17:01:33 +03:00
var portErr = qsTr ( "The port must be in the range of 1 to 65535" )
portTextField . errorText = portErr
errorLines . push ( bullet + portErr )
}
if ( ! TelemtConfigModel . isValidPublicHost ( publicHostTextField . textField . text ) ) {
var hostErr = qsTr ( "Enter a valid IP address or domain name" )
publicHostTextField . errorText = hostErr
errorLines . push ( bullet + hostErr )
}
var tagNormalized = TelemtConfigModel . sanitizeMtProxyTagFieldText ( tagTextField . textField . text )
tagTextField . textField . text = tagNormalized
if ( ! TelemtConfigModel . isValidMtProxyTag ( tagNormalized ) ) {
var tagErr = qsTr ( "Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty." )
tagTextField . errorText = tagErr
errorLines . push ( bullet + tagErr )
}
var domainValueForSave = tlsDomainTextField . textField . text === ""
? TelemtConfigModel . defaultTlsDomain ( )
: tlsDomainTextField . textField . text
if ( ! TelemtConfigModel . isValidFakeTlsDomain ( domainValueForSave ) ) {
var tlsErr = qsTr ( "Enter a valid domain name" )
tlsDomainTextField . errorText = tlsErr
errorLines . push ( bullet + tlsErr )
}
var natIpErr = qsTr ( "Enter a valid IPv4 address" )
if ( ! TelemtConfigModel . isValidOptionalIpv4 ( natInternalIpTextField . textField . text ) ) {
natInternalIpTextField . errorText = natIpErr
errorLines . push ( bullet + qsTr ( "NAT internal IP: enter a valid IPv4 address" ) )
}
if ( ! TelemtConfigModel . isValidOptionalIpv4 ( natExternalIpTextField . textField . text ) ) {
natExternalIpTextField . errorText = natIpErr
errorLines . push ( bullet + qsTr ( "NAT external IP: enter a valid IPv4 address" ) )
}
if ( errorLines . length > 0 ) {
PageController . showErrorMessage ( errorLines . join ( "\n" ) )
2026-05-18 15:01:09 +03:00
return
}
TelemtConfigModel . setPort ( portValue )
2026-06-16 17:01:33 +03:00
TelemtConfigModel . setTag ( tagNormalized )
2026-05-18 15:01:09 +03:00
TelemtConfigModel . setPublicHost ( publicHostTextField . textField . text )
TelemtConfigModel . setTransportMode ( transportMode )
var domainValue = tlsDomainTextField . textField . text === ""
? TelemtConfigModel . defaultTlsDomain ( )
: tlsDomainTextField . textField . text
TelemtConfigModel . setTlsDomain ( domainValue )
if ( transportMode === "faketls" ) {
workers = "0"
TelemtConfigModel . setWorkers ( "0" )
} else {
TelemtConfigModel . setWorkersMode ( workersMode )
TelemtConfigModel . setWorkers ( workers )
}
TelemtConfigModel . setNatEnabled ( natEnabled )
TelemtConfigModel . setNatInternalIp ( natInternalIpTextField . textField . text )
TelemtConfigModel . setNatExternalIp ( natExternalIpTextField . textField . text )
previousPort = port
previousTag = tag
previousPublicHost = publicHost
previousTransportMode = transportMode
previousTlsDomain = tlsDomain
previousWorkersMode = workersMode
previousWorkers = workers
previousNatEnabled = natEnabled
previousNatInternalIp = natInternalIp
previousNatExternalIp = natExternalIp
2026-05-28 06:46:26 +03:00
root . previousSecret = secret
2026-05-18 15:01:09 +03:00
isUpdating = true
root . telemtScheduleUpdate ( false )
}
}
}
}
}
}
}