mirror of
https://github.com/2dust/v2rayN.git
synced 2026-02-28 05:03:02 +00:00
feat: add subscription decryption support (AES-128-CBC, Subscription-Encryption header)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b5800f7dfc
commit
0dac5fde9c
15 changed files with 377 additions and 69 deletions
|
|
@ -7,6 +7,7 @@ public static class AppEvents
|
||||||
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
|
||||||
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
|
||||||
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
|
||||||
|
public static readonly EventChannel<string> SubscriptionDecryptFailedRequested = new();
|
||||||
|
|
||||||
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
|
||||||
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
|
||||||
|
|
|
||||||
|
|
@ -1669,6 +1669,7 @@ public static class ConfigHandler
|
||||||
item.Remarks = subItem.Remarks;
|
item.Remarks = subItem.Remarks;
|
||||||
item.Url = subItem.Url;
|
item.Url = subItem.Url;
|
||||||
item.MoreUrl = subItem.MoreUrl;
|
item.MoreUrl = subItem.MoreUrl;
|
||||||
|
item.LoginPassword = subItem.LoginPassword;
|
||||||
item.Enabled = subItem.Enabled;
|
item.Enabled = subItem.Enabled;
|
||||||
item.AutoUpdateInterval = subItem.AutoUpdateInterval;
|
item.AutoUpdateInterval = subItem.AutoUpdateInterval;
|
||||||
item.UserAgent = subItem.UserAgent;
|
item.UserAgent = subItem.UserAgent;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace ServiceLib.Handler;
|
namespace ServiceLib.Handler;
|
||||||
|
|
||||||
public static class SubscriptionHandler
|
public static class SubscriptionHandler
|
||||||
{
|
{
|
||||||
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Func<bool, string, Task> updateFunc)
|
public static async Task UpdateProcess(
|
||||||
|
Config config,
|
||||||
|
string subId,
|
||||||
|
bool blProxy,
|
||||||
|
Func<bool, string, Task> updateFunc,
|
||||||
|
Func<SubItem, Task>? decryptFailedFunc = null)
|
||||||
{
|
{
|
||||||
await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
|
await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
|
||||||
var subItem = await AppManager.Instance.SubItems();
|
var subItem = await AppManager.Instance.SubItems();
|
||||||
|
|
@ -35,7 +44,13 @@ public static class SubscriptionHandler
|
||||||
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
|
||||||
|
|
||||||
// Get all subscription content (main subscription + additional subscriptions)
|
// Get all subscription content (main subscription + additional subscriptions)
|
||||||
var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
|
var (result, decryptFailed) = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
|
||||||
|
if (decryptFailed)
|
||||||
|
{
|
||||||
|
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecryptFailed}");
|
||||||
|
await decryptFailedFunc?.Invoke(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Process download result
|
// Process download result
|
||||||
if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
|
if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
|
||||||
|
|
@ -90,34 +105,69 @@ public static class SubscriptionHandler
|
||||||
return downloadHandle;
|
return downloadHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> DownloadSubscriptionContent(DownloadService downloadHandle, string url, bool blProxy, string userAgent)
|
private const string SubscriptionEncryptionHeader = "Subscription-Encryption";
|
||||||
|
private const string AesEncryptionValue = "true";
|
||||||
|
|
||||||
|
private static bool IsAesEncrypted(HttpHeaders? headers)
|
||||||
{
|
{
|
||||||
var result = await downloadHandle.TryDownloadString(url, blProxy, userAgent);
|
if (headers == null || !headers.TryGetValues(SubscriptionEncryptionHeader, out var values))
|
||||||
|
return false;
|
||||||
// If download with proxy fails, try direct connection
|
return values?.Any(v => string.Equals(v.Trim(), AesEncryptionValue, StringComparison.OrdinalIgnoreCase)) == true;
|
||||||
if (blProxy && result.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
result = await downloadHandle.TryDownloadString(url, false, userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> DownloadAllSubscriptions(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
private static async Task<(string Content, HttpHeaders? Headers)> DownloadSubscriptionContentWithHeaders(DownloadService downloadHandle, string url, bool blProxy, string userAgent)
|
||||||
{
|
{
|
||||||
// Download main subscription content
|
var (content, headers) = await downloadHandle.TryDownloadStringWithHeaders(url, blProxy, userAgent);
|
||||||
var result = await DownloadMainSubscription(config, item, blProxy, downloadHandle);
|
|
||||||
|
// If download with proxy fails, try direct connection
|
||||||
|
if (blProxy && content.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
(content, headers) = await downloadHandle.TryDownloadStringWithHeaders(url, false, userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (content ?? string.Empty, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(string Result, bool DecryptFailed)> DownloadAllSubscriptions(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
||||||
|
{
|
||||||
|
var decryptFailed = false;
|
||||||
|
// Download main subscription content (with headers for encryption detection)
|
||||||
|
var (result, mainHeaders) = await DownloadMainSubscriptionWithHeaders(config, item, blProxy, downloadHandle);
|
||||||
|
if (IsAesEncrypted(mainHeaders))
|
||||||
|
{
|
||||||
|
if (item.LoginPassword.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return (string.Empty, true);
|
||||||
|
}
|
||||||
|
if (!TryDecryptSubscription(item, result, out var decrypted))
|
||||||
|
{
|
||||||
|
return (string.Empty, true);
|
||||||
|
}
|
||||||
|
result = decrypted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (result.IsNotEmpty() && Utils.IsBase64String(result))
|
||||||
|
{
|
||||||
|
result = Utils.Base64Decode(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process additional subscription links (if any)
|
// Process additional subscription links (if any)
|
||||||
if (item.ConvertTarget.IsNullOrEmpty() && item.MoreUrl.TrimEx().IsNotEmpty())
|
if (item.ConvertTarget.IsNullOrEmpty() && item.MoreUrl.TrimEx().IsNotEmpty())
|
||||||
{
|
{
|
||||||
result = await DownloadAdditionalSubscriptions(item, result, blProxy, downloadHandle);
|
var additional = await DownloadAdditionalSubscriptions(item, result, blProxy, downloadHandle);
|
||||||
|
if (additional.DecryptFailed)
|
||||||
|
{
|
||||||
|
return (string.Empty, true);
|
||||||
|
}
|
||||||
|
result = additional.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return (result, decryptFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> DownloadMainSubscription(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
private static async Task<(string Content, HttpHeaders? Headers)> DownloadMainSubscriptionWithHeaders(Config config, SubItem item, bool blProxy, DownloadService downloadHandle)
|
||||||
{
|
{
|
||||||
// Prepare subscription URL and download directly
|
// Prepare subscription URL and download directly
|
||||||
var url = Utils.GetPunycode(item.Url.TrimEx());
|
var url = Utils.GetPunycode(item.Url.TrimEx());
|
||||||
|
|
@ -142,20 +192,14 @@ public static class SubscriptionHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and return result directly
|
// Download and return result with headers
|
||||||
return await DownloadSubscriptionContent(downloadHandle, url, blProxy, item.UserAgent);
|
return await DownloadSubscriptionContentWithHeaders(downloadHandle, url, blProxy, item.UserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> DownloadAdditionalSubscriptions(SubItem item, string mainResult, bool blProxy, DownloadService downloadHandle)
|
private static async Task<(string Result, bool DecryptFailed)> DownloadAdditionalSubscriptions(SubItem item, string mainResult, bool blProxy, DownloadService downloadHandle)
|
||||||
{
|
{
|
||||||
var result = mainResult;
|
var result = mainResult;
|
||||||
|
|
||||||
// If main subscription result is Base64 encoded, decode it first
|
|
||||||
if (result.IsNotEmpty() && Utils.IsBase64String(result))
|
|
||||||
{
|
|
||||||
result = Utils.Base64Decode(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process additional URL list
|
// Process additional URL list
|
||||||
var lstUrl = item.MoreUrl.TrimEx().Split(",") ?? [];
|
var lstUrl = item.MoreUrl.TrimEx().Split(",") ?? [];
|
||||||
foreach (var it in lstUrl)
|
foreach (var it in lstUrl)
|
||||||
|
|
@ -166,23 +210,110 @@ public static class SubscriptionHandler
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var additionalResult = await DownloadSubscriptionContent(downloadHandle, url2, blProxy, item.UserAgent);
|
var (additionalContent, additionalHeaders) = await DownloadSubscriptionContentWithHeaders(downloadHandle, url2, blProxy, item.UserAgent);
|
||||||
|
|
||||||
if (additionalResult.IsNotEmpty())
|
if (additionalContent.IsNotEmpty())
|
||||||
{
|
{
|
||||||
// Process additional subscription results, add to main result
|
// Check header for encryption; decrypt only when Subscription-Encryption present
|
||||||
if (Utils.IsBase64String(additionalResult))
|
if (IsAesEncrypted(additionalHeaders))
|
||||||
{
|
{
|
||||||
result += Environment.NewLine + Utils.Base64Decode(additionalResult);
|
if (item.LoginPassword.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return (string.Empty, true);
|
||||||
|
}
|
||||||
|
if (!TryDecryptSubscription(item, additionalContent, out var decrypted))
|
||||||
|
{
|
||||||
|
return (string.Empty, true);
|
||||||
|
}
|
||||||
|
result += Environment.NewLine + decrypted;
|
||||||
|
}
|
||||||
|
else if (Utils.IsBase64String(additionalContent))
|
||||||
|
{
|
||||||
|
result += Environment.NewLine + Utils.Base64Decode(additionalContent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result += Environment.NewLine + additionalResult;
|
result += Environment.NewLine + additionalContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return (result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryDecryptSubscription(SubItem item, string base64Data, out string decrypted)
|
||||||
|
{
|
||||||
|
decrypted = string.Empty;
|
||||||
|
if (base64Data.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pass = Utils.GetMd5(item.LoginPassword ?? string.Empty);
|
||||||
|
if (pass.Length != 32)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = Convert.FromHexString(pass);
|
||||||
|
var raw = TryBase64DecodeBytes(base64Data);
|
||||||
|
if (raw == null || raw.Length <= 16)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iv = raw[..16];
|
||||||
|
var cipher = raw[16..];
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = key;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
var plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||||
|
decrypted = Encoding.UTF8.GetString(plainBytes);
|
||||||
|
return decrypted.IsNotEmpty();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog("SubscriptionDecryptFailed", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[]? TryBase64DecodeBytes(string plainText)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (plainText.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
plainText = plainText.Trim()
|
||||||
|
.Replace(Environment.NewLine, "")
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("\r", "")
|
||||||
|
.Replace('_', '/')
|
||||||
|
.Replace('-', '+')
|
||||||
|
.Replace(" ", "");
|
||||||
|
|
||||||
|
if (plainText.Length % 4 > 0)
|
||||||
|
{
|
||||||
|
plainText = plainText.PadRight(plainText.Length + 4 - (plainText.Length % 4), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.FromBase64String(plainText);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog("SubscriptionBase64DecodeFailed", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
|
private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ public class SubItem
|
||||||
|
|
||||||
public string MoreUrl { get; set; }
|
public string MoreUrl { get; set; }
|
||||||
|
|
||||||
|
public string? LoginPassword { get; set; }
|
||||||
|
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
public string UserAgent { get; set; } = string.Empty;
|
public string UserAgent { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
26
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
26
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -1,4 +1,4 @@
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// 此代码由工具生成。
|
// 此代码由工具生成。
|
||||||
// 运行时版本:4.0.30319.42000
|
// 运行时版本:4.0.30319.42000
|
||||||
|
|
@ -510,6 +510,15 @@ namespace ServiceLib.Resx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Login password 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string LvLoginPassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LvLoginPassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Remarks Memo 的本地化字符串。
|
/// 查找类似 Remarks Memo 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2049,6 +2058,15 @@ namespace ServiceLib.Resx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Subscription decryption failed 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string MsgSubscriptionDecryptFailed {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MsgSubscriptionDecryptFailed", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Unpacking... 的本地化字符串。
|
/// 查找类似 Unpacking... 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2430,6 +2448,12 @@ namespace ServiceLib.Resx {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 For group please leave blank here 的本地化字符串。
|
/// 查找类似 For group please leave blank here 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public static string SubLoginPasswordTips {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SubLoginPasswordTips", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string SubUrlTips {
|
public static string SubUrlTips {
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("SubUrlTips", resourceCulture);
|
return ResourceManager.GetString("SubUrlTips", resourceCulture);
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,9 @@
|
||||||
<data name="MsgSubscriptionDecodingFailed" xml:space="preserve">
|
<data name="MsgSubscriptionDecodingFailed" xml:space="preserve">
|
||||||
<value>Invalid subscription content</value>
|
<value>Invalid subscription content</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MsgSubscriptionDecryptFailed" xml:space="preserve">
|
||||||
|
<value>Subscription decryption failed</value>
|
||||||
|
</data>
|
||||||
<data name="MsgUnpacking" xml:space="preserve">
|
<data name="MsgUnpacking" xml:space="preserve">
|
||||||
<value>Unpacking...</value>
|
<value>Unpacking...</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -309,6 +312,9 @@
|
||||||
<data name="LvRemarks" xml:space="preserve">
|
<data name="LvRemarks" xml:space="preserve">
|
||||||
<value>Remarks</value>
|
<value>Remarks</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvLoginPassword" xml:space="preserve">
|
||||||
|
<value>Login password</value>
|
||||||
|
</data>
|
||||||
<data name="LvUrl" xml:space="preserve">
|
<data name="LvUrl" xml:space="preserve">
|
||||||
<value>URL (optional)</value>
|
<value>URL (optional)</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -867,6 +873,9 @@
|
||||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||||
<value>Supports DNS Object; Click to view documentation</value>
|
<value>Supports DNS Object; Click to view documentation</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubLoginPasswordTips" xml:space="preserve">
|
||||||
|
<value>Please enter the website login password</value>
|
||||||
|
</data>
|
||||||
<data name="SubUrlTips" xml:space="preserve">
|
<data name="SubUrlTips" xml:space="preserve">
|
||||||
<value>For group please leave blank here</value>
|
<value>For group please leave blank here</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,9 @@
|
||||||
<data name="MsgSubscriptionDecodingFailed" xml:space="preserve">
|
<data name="MsgSubscriptionDecodingFailed" xml:space="preserve">
|
||||||
<value>无效的订阅内容</value>
|
<value>无效的订阅内容</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MsgSubscriptionDecryptFailed" xml:space="preserve">
|
||||||
|
<value>订阅解密失败</value>
|
||||||
|
</data>
|
||||||
<data name="MsgUnpacking" xml:space="preserve">
|
<data name="MsgUnpacking" xml:space="preserve">
|
||||||
<value>正在解压......</value>
|
<value>正在解压......</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -309,6 +312,9 @@
|
||||||
<data name="LvRemarks" xml:space="preserve">
|
<data name="LvRemarks" xml:space="preserve">
|
||||||
<value>别名</value>
|
<value>别名</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LvLoginPassword" xml:space="preserve">
|
||||||
|
<value>登录密码</value>
|
||||||
|
</data>
|
||||||
<data name="LvUrl" xml:space="preserve">
|
<data name="LvUrl" xml:space="preserve">
|
||||||
<value>可选地址 (Url)</value>
|
<value>可选地址 (Url)</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -867,6 +873,9 @@
|
||||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||||
<value>支持填写 DnsObject,JSON 格式,点击查看文档</value>
|
<value>支持填写 DnsObject,JSON 格式,点击查看文档</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubLoginPasswordTips" xml:space="preserve">
|
||||||
|
<value>请输入网站登录密码</value>
|
||||||
|
</data>
|
||||||
<data name="SubUrlTips" xml:space="preserve">
|
<data name="SubUrlTips" xml:space="preserve">
|
||||||
<value>普通分组此处请留空</value>
|
<value>普通分组此处请留空</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -87,13 +87,22 @@ public class DownloadService
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
||||||
|
{
|
||||||
|
var (content, _) = await TryDownloadStringWithHeaders(url, blProxy, userAgent);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download string and return response headers (for subscription encryption detection).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string? Content, HttpHeaders? ResponseHeaders)> TryDownloadStringWithHeaders(string url, bool blProxy, string userAgent)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15);
|
var (content, headers) = await DownloadStringWithHeadersAsync(url, blProxy, userAgent, 15);
|
||||||
if (result1.IsNotEmpty())
|
if (content.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result1;
|
return (content, headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -111,7 +120,7 @@ public class DownloadService
|
||||||
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
|
var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15);
|
||||||
if (result2.IsNotEmpty())
|
if (result2.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result2;
|
return (result2, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -124,7 +133,7 @@ public class DownloadService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -132,6 +141,12 @@ public class DownloadService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url"></param>
|
/// <param name="url"></param>
|
||||||
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
|
private async Task<string?> DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout)
|
||||||
|
{
|
||||||
|
var (content, _) = await DownloadStringWithHeadersAsync(url, blProxy, userAgent, timeout);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string? Content, HttpHeaders? ResponseHeaders)> DownloadStringWithHeadersAsync(string url, bool blProxy, string userAgent, int timeout)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -156,8 +171,10 @@ public class DownloadService
|
||||||
}
|
}
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
|
using var response = await client.GetAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token);
|
||||||
return result;
|
response.EnsureSuccessStatusCode();
|
||||||
|
var content = await response.Content.ReadAsStringAsync(cts.Token);
|
||||||
|
return (content, response.Headers);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -168,7 +185,7 @@ public class DownloadService
|
||||||
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
Error?.Invoke(this, new ErrorEventArgs(ex.InnerException));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,22 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
|
|
||||||
public async Task UpdateSubscriptionProcess(string subId, bool blProxy)
|
public async Task UpdateSubscriptionProcess(string subId, bool blProxy)
|
||||||
{
|
{
|
||||||
await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler));
|
var decryptPromptShown = false;
|
||||||
|
await Task.Run(async () => await SubscriptionHandler.UpdateProcess(
|
||||||
|
_config,
|
||||||
|
subId,
|
||||||
|
blProxy,
|
||||||
|
UpdateTaskHandler,
|
||||||
|
async subItem =>
|
||||||
|
{
|
||||||
|
if (decryptPromptShown || subItem?.Id.IsNullOrEmpty() != false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
decryptPromptShown = true;
|
||||||
|
AppEvents.SubscriptionDecryptFailedRequested.Publish(subItem.Id);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Subscription
|
#endregion Subscription
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,12 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(blShow => ShowHideWindow(blShow))
|
.Subscribe(blShow => ShowHideWindow(blShow))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
AppEvents.SubscriptionDecryptFailedRequested
|
||||||
|
.AsObservable()
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Subscribe(async subId => await OpenSubEditForDecryptFailed(subId))
|
||||||
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Utils.IsWindows())
|
if (Utils.IsWindows())
|
||||||
|
|
@ -187,6 +193,21 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OpenSubEditForDecryptFailed(string subId)
|
||||||
|
{
|
||||||
|
if (subId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lst = await AppManager.Instance.SubItems();
|
||||||
|
var item = lst?.FirstOrDefault(x => x.Id == subId);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new SubEditWindow(item, focusLoginPassword: true).ShowDialog<bool>(this);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,21 @@
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Watermark="{x:Static resx:ResUI.SubUrlTips}" />
|
Watermark="{x:Static resx:ResUI.SubUrlTips}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.LvLoginPassword}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtLoginPassword"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
PasswordChar="*"
|
||||||
|
Watermark="{x:Static resx:ResUI.SubLoginPasswordTips}" />
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
|
|
@ -106,14 +121,14 @@
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="3"
|
Grid.Row="4"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvEnabled}" />
|
Text="{x:Static resx:ResUI.LvEnabled}" />
|
||||||
|
|
||||||
<DockPanel
|
<DockPanel
|
||||||
Grid.Row="3"
|
Grid.Row="4"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}">
|
Margin="{StaticResource Margin4}">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ namespace v2rayN.Desktop.Views;
|
||||||
|
|
||||||
public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||||
{
|
{
|
||||||
|
private readonly bool _focusLoginPassword;
|
||||||
|
|
||||||
public SubEditWindow()
|
public SubEditWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubEditWindow(SubItem subItem)
|
public SubEditWindow(SubItem subItem, bool focusLoginPassword = false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_focusLoginPassword = focusLoginPassword;
|
||||||
|
|
||||||
Loaded += Window_Loaded;
|
Loaded += Window_Loaded;
|
||||||
btnCancel.Click += (s, e) => Close();
|
btnCancel.Click += (s, e) => Close();
|
||||||
|
|
@ -24,6 +27,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||||
{
|
{
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Url, v => v.txtUrl.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Url, v => v.txtUrl.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.LoginPassword, v => v.txtLoginPassword.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.MoreUrl, v => v.txtMoreUrl.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.MoreUrl, v => v.txtMoreUrl.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
||||||
|
|
@ -53,7 +57,15 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||||
|
|
||||||
private void Window_Loaded(object? sender, RoutedEventArgs e)
|
private void Window_Loaded(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
txtRemarks.Focus();
|
if (_focusLoginPassword)
|
||||||
|
{
|
||||||
|
txtLoginPassword.Focus();
|
||||||
|
txtLoginPassword.SelectAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
txtRemarks.Focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
|
private async void BtnSelectPrevProfile_Click(object? sender, RoutedEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,12 @@ public partial class MainWindow
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
.Subscribe(blShow => ShowHideWindow(blShow))
|
.Subscribe(blShow => ShowHideWindow(blShow))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
AppEvents.SubscriptionDecryptFailedRequested
|
||||||
|
.AsObservable()
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Subscribe(async subId => await OpenSubEditForDecryptFailed(subId))
|
||||||
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
|
Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
|
||||||
|
|
@ -181,6 +187,22 @@ public partial class MainWindow
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OpenSubEditForDecryptFailed(string subId)
|
||||||
|
{
|
||||||
|
if (subId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lst = await AppManager.Instance.SubItems();
|
||||||
|
var item = lst?.FirstOrDefault(x => x.Id == subId);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new SubEditWindow(item, focusLoginPassword: true).ShowDialog();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
@ -145,10 +146,26 @@
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource ToolbarTextBlock}"
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
|
Text="{x:Static resx:ResUI.LvLoginPassword}" />
|
||||||
|
<TextBox
|
||||||
|
x:Name="txtLoginPassword"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.SubLoginPasswordTips}"
|
||||||
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource ToolbarTextBlock}"
|
||||||
Text="{x:Static resx:ResUI.LvEnabled}" />
|
Text="{x:Static resx:ResUI.LvEnabled}" />
|
||||||
|
|
||||||
<DockPanel
|
<DockPanel
|
||||||
Grid.Row="3"
|
Grid.Row="4"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}">
|
Margin="{StaticResource Margin4}">
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
|
|
@ -176,7 +193,7 @@
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="5"
|
Grid.Row="6"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -184,7 +201,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvFilter}" />
|
Text="{x:Static resx:ResUI.LvFilter}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtFilter"
|
x:Name="txtFilter"
|
||||||
Grid.Row="5"
|
Grid.Row="6"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -193,7 +210,7 @@
|
||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="6"
|
Grid.Row="7"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -201,7 +218,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvConvertTarget}" />
|
Text="{x:Static resx:ResUI.LvConvertTarget}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
x:Name="cmbConvertTarget"
|
x:Name="cmbConvertTarget"
|
||||||
Grid.Row="6"
|
Grid.Row="7"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvConvertTargetTip}"
|
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.LvConvertTargetTip}"
|
||||||
|
|
@ -209,14 +226,14 @@
|
||||||
Style="{StaticResource MyOutlinedTextComboBox}" />
|
Style="{StaticResource MyOutlinedTextComboBox}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="7"
|
Grid.Row="8"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvUserAgent}" />
|
Text="{x:Static resx:ResUI.LvUserAgent}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtUserAgent"
|
x:Name="txtUserAgent"
|
||||||
Grid.Row="7"
|
Grid.Row="8"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -226,7 +243,7 @@
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="8"
|
Grid.Row="9"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -234,7 +251,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvSort}" />
|
Text="{x:Static resx:ResUI.LvSort}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtSort"
|
x:Name="txtSort"
|
||||||
Grid.Row="8"
|
Grid.Row="9"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="100"
|
Width="100"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
|
|
@ -244,7 +261,7 @@
|
||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -252,7 +269,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPrevProfile"
|
x:Name="txtPrevProfile"
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -260,7 +277,7 @@
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="9"
|
Grid.Row="10"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -269,7 +286,7 @@
|
||||||
Style="{StaticResource DefButton}" />
|
Style="{StaticResource DefButton}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -277,7 +294,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtNextProfile"
|
x:Name="txtNextProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -285,7 +302,7 @@
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
Style="{StaticResource MyOutlinedTextBox}" />
|
Style="{StaticResource MyOutlinedTextBox}" />
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="10"
|
Grid.Row="11"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -294,7 +311,7 @@
|
||||||
Style="{StaticResource DefButton}" />
|
Style="{StaticResource DefButton}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -302,7 +319,7 @@
|
||||||
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtPreSocksPort"
|
x:Name="txtPreSocksPort"
|
||||||
Grid.Row="11"
|
Grid.Row="12"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
|
|
@ -312,7 +329,7 @@
|
||||||
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
@ -320,7 +337,7 @@
|
||||||
Text="{x:Static resx:ResUI.LvMemo}" />
|
Text="{x:Static resx:ResUI.LvMemo}" />
|
||||||
<TextBox
|
<TextBox
|
||||||
x:Name="txtMemo"
|
x:Name="txtMemo"
|
||||||
Grid.Row="12"
|
Grid.Row="13"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ namespace v2rayN.Views;
|
||||||
|
|
||||||
public partial class SubEditWindow
|
public partial class SubEditWindow
|
||||||
{
|
{
|
||||||
public SubEditWindow(SubItem subItem)
|
private readonly bool _focusLoginPassword;
|
||||||
|
|
||||||
|
public SubEditWindow(SubItem subItem, bool focusLoginPassword = false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_focusLoginPassword = focusLoginPassword;
|
||||||
|
|
||||||
Owner = Application.Current.MainWindow;
|
Owner = Application.Current.MainWindow;
|
||||||
Loaded += Window_Loaded;
|
Loaded += Window_Loaded;
|
||||||
|
|
@ -17,6 +20,7 @@ public partial class SubEditWindow
|
||||||
{
|
{
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Url, v => v.txtUrl.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Url, v => v.txtUrl.Text).DisposeWith(disposables);
|
||||||
|
this.Bind(ViewModel, vm => vm.SelectedSource.LoginPassword, v => v.txtLoginPassword.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.MoreUrl, v => v.txtMoreUrl.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.MoreUrl, v => v.txtMoreUrl.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
|
||||||
|
|
@ -47,7 +51,15 @@ public partial class SubEditWindow
|
||||||
|
|
||||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
txtRemarks.Focus();
|
if (_focusLoginPassword)
|
||||||
|
{
|
||||||
|
txtLoginPassword.Focus();
|
||||||
|
txtLoginPassword.SelectAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
txtRemarks.Focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
|
private async void BtnSelectPrevProfile_Click(object sender, RoutedEventArgs e)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue