@@ -20,6 +20,8 @@ import org.monogram.domain.models.toDomainProxyType
2020import org.monogram.domain.models.toProxyModel
2121import org.monogram.domain.repository.AppPreferencesProvider
2222import org.monogram.domain.repository.DEFAULT_SMART_SWITCH_CHECK_INTERVAL_MINUTES
23+ import org.monogram.domain.repository.LinkAction
24+ import org.monogram.domain.repository.LinkHandlerRepository
2325import org.monogram.domain.repository.ProxyDiagnosticsRepository
2426import org.monogram.domain.repository.ProxyNetworkMode
2527import org.monogram.domain.repository.ProxyNetworkRule
@@ -28,6 +30,7 @@ import org.monogram.domain.repository.ProxyRepository
2830import org.monogram.domain.repository.ProxySmartSwitchMode
2931import org.monogram.domain.repository.ProxySortMode
3032import org.monogram.domain.repository.ProxyUnavailableFallback
33+ import org.monogram.domain.repository.StringProvider
3134import org.monogram.domain.repository.defaultProxyNetworkMode
3235import org.monogram.presentation.core.util.coRunCatching
3336import org.monogram.presentation.core.util.componentScope
@@ -62,6 +65,7 @@ interface ProxyComponent {
6265 fun onToggleFavoriteProxy (proxyId : Int )
6366 fun exportProxiesJson (): String
6467 fun importProxiesJson (json : String )
68+ fun importProxiesFromText (rawText : String )
6569 fun onProxyNetworkModeChanged (networkType : ProxyNetworkType , mode : ProxyNetworkMode )
6670 fun onSpecificProxyForNetworkSelected (networkType : ProxyNetworkType , proxyId : Int )
6771 fun onClearUnavailableProxies ()
@@ -110,6 +114,9 @@ class DefaultProxyComponent(
110114
111115 private val appPreferences: AppPreferencesProvider = container.preferences.appPreferences
112116 private val proxyRepository: ProxyRepository = container.repositories.proxyRepository
117+ private val linkHandlerRepository: LinkHandlerRepository =
118+ container.repositories.linkHandlerRepository
119+ private val stringProvider: StringProvider = container.utils.stringProvider()
113120 private val proxyDiagnosticsRepository: ProxyDiagnosticsRepository =
114121 container.repositories.proxyDiagnosticsRepository
115122
@@ -122,6 +129,10 @@ class DefaultProxyComponent(
122129 private var pingRequestToken = 0L
123130 private val pingRequestByProxyId = mutableMapOf<Int , Long >()
124131
132+ private fun tr (resName : String , vararg args : Any ): String {
133+ return stringProvider.getString(resName, * args)
134+ }
135+
125136 private fun showToastThrottled (message : String , throttleMs : Long = 1500L) {
126137 val now = System .currentTimeMillis()
127138 val isDuplicateTooSoon = lastToastMessage == message && (now - lastToastAtMs) < throttleMs
@@ -146,7 +157,7 @@ class DefaultProxyComponent(
146157 coRunCatching { refreshProxies(shouldPing = true ) }
147158 .onFailure {
148159 _state .update { state -> state.copy(isLoading = false ) }
149- showToastThrottled(" Failed to load proxies " )
160+ showToastThrottled(tr( " proxy_load_failed " ) )
150161 }
151162 }
152163
@@ -242,7 +253,7 @@ class DefaultProxyComponent(
242253 val restoredProxies = coRunCatching { restoreUserProxiesIfNeeded() }
243254 .getOrElse { emptyList() }
244255 val allProxies = coRunCatching { proxyRepository.getProxies().map { it.toProxyModel() } }
245- .onFailure { showToastThrottled(" Failed to load proxies " ) }
256+ .onFailure { showToastThrottled(tr( " proxy_load_failed " ) ) }
246257 .getOrElse { emptyList() }
247258 .ifEmpty { restoredProxies }
248259 _state .update {
@@ -585,7 +596,7 @@ class DefaultProxyComponent(
585596 override fun importProxiesJson (json : String ) {
586597 scope.launch {
587598 val existing = coRunCatching { proxyRepository.getProxies().map { it.toProxyModel() } }
588- .onFailure { showToastThrottled(" Failed to load existing proxies " ) }
599+ .onFailure { showToastThrottled(tr( " proxy_existing_load_failed " ) ) }
589600 .getOrElse { emptyList() }
590601 val fingerprintToId = existing.associate { proxy ->
591602 proxyFingerprint(proxy.server, proxy.port, proxy.type) to proxy.id
@@ -609,7 +620,7 @@ class DefaultProxyComponent(
609620 }.getOrNull()
610621
611622 if (parsedEntries == null ) {
612- _state .update { it.copy(toastMessage = " Import failed: invalid file " ) }
623+ _state .update { it.copy(toastMessage = tr( " proxy_import_invalid_file " ) ) }
613624 return @launch
614625 }
615626
@@ -659,11 +670,156 @@ class DefaultProxyComponent(
659670 favoriteProxyIdToSet?.let { appPreferences.setFavoriteProxyId(it) }
660671 refreshProxies(shouldPing = true )
661672 _state .update {
662- it.copy(toastMessage = " Imported: $ added , skipped: $skipped , invalid: $invalid " )
673+ it.copy(toastMessage = tr( " proxy_import_summary_format " , added, skipped, invalid) )
663674 }
664675 }
665676 }
666677
678+ override fun importProxiesFromText (rawText : String ) {
679+ scope.launch {
680+ val normalized = rawText.trim()
681+ if (normalized.isBlank()) {
682+ showToastThrottled(tr(" proxy_clipboard_empty" ))
683+ return @launch
684+ }
685+
686+ val candidates = extractProxyLinkCandidates(rawText)
687+ if (candidates.isEmpty()) {
688+ showToastThrottled(tr(" proxy_no_links_found" ))
689+ return @launch
690+ }
691+
692+ val existing = coRunCatching { proxyRepository.getProxies().map { it.toProxyModel() } }
693+ .onFailure { showToastThrottled(tr(" proxy_existing_load_failed" )) }
694+ .getOrElse { emptyList() }
695+ val knownFingerprints = existing
696+ .mapTo(mutableSetOf ()) { proxyFingerprint(it.server, it.port, it.type) }
697+
698+ var added = 0
699+ var skipped = 0
700+ var invalid = 0
701+
702+ candidates.forEach { candidate ->
703+ val action = runCatching { linkHandlerRepository.handleLink(candidate) }.getOrNull()
704+ val proxy = action as ? LinkAction .AddProxy
705+ if (proxy == null ) {
706+ invalid++
707+ return @forEach
708+ }
709+
710+ val fingerprint = proxyFingerprint(proxy.server, proxy.port, proxy.type)
711+ if (! knownFingerprints.add(fingerprint)) {
712+ skipped++
713+ return @forEach
714+ }
715+
716+ val addedProxy = proxyRepository.addProxy(
717+ input = ProxyInput (
718+ server = proxy.server,
719+ port = proxy.port,
720+ type = proxy.type.toDomainProxyType()
721+ ),
722+ enable = false
723+ )?.toProxyModel()
724+
725+ if (addedProxy != null ) {
726+ addProxyToBackup(addedProxy)
727+ added++
728+ } else {
729+ knownFingerprints.remove(fingerprint)
730+ invalid++
731+ }
732+ }
733+
734+ if (added > 0 ) {
735+ refreshProxies(shouldPing = true )
736+ }
737+
738+ showToastThrottled(tr(" proxy_import_summary_format" , added, skipped, invalid))
739+ }
740+ }
741+
742+ private fun extractProxyLinkCandidates (rawText : String ): List <String > {
743+ val linkRegex = Regex (
744+ pattern = """ (?i)(tg:(?://)?[^\s]+|https?://(?:t\.me|www\.t\.me|telegram\.me|www\.telegram\.me)/[^\s]+)"""
745+ )
746+ val fromRegex = linkRegex.findAll(rawText).map { it.value }
747+ val fromLines = rawText.lineSequence()
748+ return (fromRegex + fromLines)
749+ .map { normalizeForParsing(it) }
750+ .map { normalizeTelegramScheme(it) }
751+ .filter { it.isNotBlank() && looksLikeProxyLink(it) }
752+ .distinct()
753+ .toList()
754+ }
755+
756+ private fun normalizeTelegramScheme (link : String ): String {
757+ if (link.startsWith(" tg://" , ignoreCase = true )) return link
758+ if (link.startsWith(" tg:" , ignoreCase = true )) {
759+ return " tg://${link.substringAfter(' :' )} "
760+ }
761+ return link
762+ }
763+
764+ private fun normalizeForParsing (link : String ): String {
765+ var sanitized = link.trim()
766+ .removeSurrounding(" <" , " >" )
767+ .removeSurrounding(" \" " )
768+ .removeSurrounding(" '" )
769+
770+ while (sanitized.isNotEmpty() && sanitized.last() in setOf (
771+ ' )' ,
772+ ' ]' ,
773+ ' }' ,
774+ ' .' ,
775+ ' ,' ,
776+ ' ;' ,
777+ ' !' ,
778+ ' ?'
779+ )
780+ ) {
781+ sanitized = sanitized.dropLast(1 )
782+ }
783+ return sanitized
784+ }
785+
786+ private fun looksLikeProxyLink (link : String ): Boolean {
787+ val linkLower = link.lowercase()
788+ return linkLower.startsWith(" tg://proxy?" ) ||
789+ linkLower.startsWith(" tg:proxy?" ) ||
790+ linkLower.startsWith(" tg://proxy/" ) ||
791+ linkLower.startsWith(" tg://socks?" ) ||
792+ linkLower.startsWith(" tg:socks?" ) ||
793+ linkLower.startsWith(" tg://socks/" ) ||
794+ linkLower.startsWith(" tg://http?" ) ||
795+ linkLower.startsWith(" tg:http?" ) ||
796+ linkLower.startsWith(" tg://http/" ) ||
797+ linkLower.startsWith(" https://t.me/proxy?" ) ||
798+ linkLower.startsWith(" http://t.me/proxy?" ) ||
799+ linkLower.startsWith(" https://www.t.me/proxy?" ) ||
800+ linkLower.startsWith(" http://www.t.me/proxy?" ) ||
801+ linkLower.startsWith(" https://telegram.me/proxy?" ) ||
802+ linkLower.startsWith(" http://telegram.me/proxy?" ) ||
803+ linkLower.startsWith(" https://www.telegram.me/proxy?" ) ||
804+ linkLower.startsWith(" http://www.telegram.me/proxy?" ) ||
805+ linkLower.startsWith(" https://t.me/socks?" ) ||
806+ linkLower.startsWith(" http://t.me/socks?" ) ||
807+ linkLower.startsWith(" https://www.t.me/socks?" ) ||
808+ linkLower.startsWith(" http://www.t.me/socks?" ) ||
809+ linkLower.startsWith(" https://telegram.me/socks?" ) ||
810+ linkLower.startsWith(" http://telegram.me/socks?" ) ||
811+ linkLower.startsWith(" https://www.telegram.me/socks?" ) ||
812+ linkLower.startsWith(" http://www.telegram.me/socks?" ) ||
813+ linkLower.startsWith(" https://t.me/http?" ) ||
814+ linkLower.startsWith(" http://t.me/http?" ) ||
815+ linkLower.startsWith(" https://www.t.me/http?" ) ||
816+ linkLower.startsWith(" http://www.t.me/http?" ) ||
817+ linkLower.startsWith(" https://telegram.me/http?" ) ||
818+ linkLower.startsWith(" http://telegram.me/http?" ) ||
819+ linkLower.startsWith(" https://www.telegram.me/http?" ) ||
820+ linkLower.startsWith(" http://www.telegram.me/http?" )
821+ }
822+
667823 override fun onEnableProxy (proxyId : Int ) {
668824 scope.launch {
669825 if (proxyRepository.enableProxy(proxyId)) {
@@ -874,7 +1030,7 @@ class DefaultProxyComponent(
8741030 upsertProxyLocally(proxy)
8751031 onPingProxy(proxy.id)
8761032 } else {
877- showToastThrottled(" Failed to add proxy " )
1033+ showToastThrottled(tr( " proxy_add_failed " ) )
8781034 }
8791035 }
8801036 }
@@ -901,7 +1057,7 @@ class DefaultProxyComponent(
9011057 upsertProxyLocally(proxy, replaceId = proxyId, closeEditor = true )
9021058 onPingProxy(proxy.id)
9031059 } else {
904- showToastThrottled(" Failed to save proxy " )
1060+ showToastThrottled(tr( " proxy_save_failed " ) )
9051061 }
9061062 }
9071063 }
0 commit comments