Compare commits

...

4 commits

Author SHA1 Message Date
DHR60
0f3a3eac02
Group preview (#8760)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
release Linux / rpm (push) Has been cancelled
* Group Preview

* Fix
2026-02-06 14:33:58 +08:00
2dust
54608ab2b9 Adjust GroupProfileManager 2026-02-06 14:15:21 +08:00
2dust
6167624443 Rename ProfileItems to ProfileModels and refactor 2026-02-06 13:50:47 +08:00
2dust
7a58e78381 Refactor profile migration and add group handling 2026-02-06 11:45:26 +08:00
24 changed files with 449 additions and 140 deletions

View file

@ -844,7 +844,7 @@ public static class ConfigHandler
/// <returns>0 if successful, -1 if failed</returns> /// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> SortServers(Config config, string subId, string colName, bool asc) public static async Task<int> SortServers(Config config, string subId, string colName, bool asc)
{ {
var lstModel = await AppManager.Instance.ProfileItems(subId, ""); var lstModel = await AppManager.Instance.ProfileModels(subId, "");
if (lstModel.Count <= 0) if (lstModel.Count <= 0)
{ {
return -1; return -1;
@ -1213,7 +1213,8 @@ public static class ConfigHandler
} }
var extraItem = new ProtocolExtraItem var extraItem = new ProtocolExtraItem
{ {
ChildItems = childProfileIndexId, MultipleLoad = multipleLoad, ChildItems = childProfileIndexId,
MultipleLoad = multipleLoad,
}; };
profile.SetProtocolExtra(extraItem); profile.SetProtocolExtra(extraItem);
var ret = await AddServerCommon(config, profile, true); var ret = await AddServerCommon(config, profile, true);
@ -1277,7 +1278,7 @@ public static class ConfigHandler
/// <returns>Number of removed servers or -1 if failed</returns> /// <returns>Number of removed servers or -1 if failed</returns>
public static async Task<int> RemoveInvalidServerResult(Config config, string subid) public static async Task<int> RemoveInvalidServerResult(Config config, string subid)
{ {
var lstModel = await AppManager.Instance.ProfileItems(subid, ""); var lstModel = await AppManager.Instance.ProfileModels(subid, "");
if (lstModel is { Count: <= 0 }) if (lstModel is { Count: <= 0 })
{ {
return -1; return -1;

View file

@ -191,10 +191,17 @@ public sealed class AppManager
return (await ProfileItems(subid))?.Select(t => t.IndexId)?.ToList(); return (await ProfileItems(subid))?.Select(t => t.IndexId)?.ToList();
} }
public async Task<List<ProfileItemModel>?> ProfileItems(string subid, string filter) public async Task<List<ProfileItemModel>?> ProfileModels(string subid, string filter)
{ {
var sql = @$"select a.* var sql = @$"select a.IndexId
,b.remarks subRemarks ,a.ConfigType
,a.Remarks
,a.Address
,a.Port
,a.Network
,a.StreamSecurity
,a.Subid
,b.remarks as subRemarks
from ProfileItem a from ProfileItem a
left join SubItem b on a.subid = b.id left join SubItem b on a.subid = b.id
where 1=1 "; where 1=1 ";
@ -264,107 +271,26 @@ public sealed class AppManager
public async Task MigrateProfileExtra() public async Task MigrateProfileExtra()
{ {
#pragma warning disable CS0618 await MigrateProfileExtraGroup();
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
var groupItems = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!));
const int pageSize = 500; #pragma warning disable CS0618
const int pageSize = 100;
var offset = 0; var offset = 0;
while (true) while (true)
{ {
var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 LIMIT {pageSize} OFFSET {offset}"; var sql = $"SELECT * FROM ProfileItem " +
$"WHERE ConfigVersion < 3 " +
$"AND ConfigType NOT IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain}) " +
$"LIMIT {pageSize} OFFSET {offset}";
var batch = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql); var batch = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql);
if (batch is null || batch.Count == 0) if (batch is null || batch.Count == 0)
{ {
break; break;
} }
var batchSuccessCount = 0; var batchSuccessCount = await MigrateProfileExtraSub(batch);
foreach (var item in batch)
{
try
{
var extra = item.GetProtocolExtra();
if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
extra = extra with { GroupType = nameof(item.ConfigType) };
groupItems.TryGetValue(item.IndexId, out var groupItem);
if (groupItem != null && !groupItem.NotHasChild())
{
extra = extra with
{
ChildItems = groupItem.ChildItems,
SubChildItems = groupItem.SubChildItems,
Filter = groupItem.Filter,
MultipleLoad = groupItem.MultipleLoad,
};
}
}
switch (item.ConfigType)
{
case EConfigType.Shadowsocks:
extra = extra with { SsMethod = item.Security.NullIfEmpty() };
break;
case EConfigType.VMess:
extra = extra with
{
AlterId = item.AlterId.ToString(),
VmessSecurity = item.Security.NullIfEmpty(),
};
break;
case EConfigType.VLESS:
extra = extra with
{
Flow = item.Flow.NullIfEmpty(),
VlessEncryption = item.Security,
};
break;
case EConfigType.Hysteria2:
extra = extra with
{
SalamanderPass = item.Path.NullIfEmpty(),
Ports = item.Ports.NullIfEmpty(),
UpMbps = _config.HysteriaItem.UpMbps,
DownMbps = _config.HysteriaItem.DownMbps,
HopInterval = _config.HysteriaItem.HopInterval.ToString(),
};
break;
case EConfigType.TUIC:
item.Username = item.Id;
item.Id = item.Security;
item.Password = item.Security;
break;
case EConfigType.HTTP:
case EConfigType.SOCKS:
item.Username = item.Security;
break;
case EConfigType.WireGuard:
extra = extra with
{
WgPublicKey = item.PublicKey.NullIfEmpty(),
WgInterfaceAddress = item.RequestHost.NullIfEmpty(),
WgReserved = item.Path.NullIfEmpty(),
WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280
};
break;
}
item.SetProtocolExtra(extra);
item.Password = item.Id;
item.ConfigVersion = 3;
await SQLiteHelper.Instance.UpdateAsync(item);
batchSuccessCount++;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtra Error: {ex}");
}
}
// Only increment offset by the number of failed items that remain in the result set // Only increment offset by the number of failed items that remain in the result set
// Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3 // Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3
@ -375,6 +301,173 @@ public sealed class AppManager
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
private async Task<int> MigrateProfileExtraSub(List<ProfileItem> batch)
{
var updateProfileItems = new List<ProfileItem>();
foreach (var item in batch)
{
try
{
var extra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.Shadowsocks:
extra = extra with { SsMethod = item.Security.NullIfEmpty() };
break;
case EConfigType.VMess:
extra = extra with
{
AlterId = item.AlterId.ToString(),
VmessSecurity = item.Security.NullIfEmpty(),
};
break;
case EConfigType.VLESS:
extra = extra with
{
Flow = item.Flow.NullIfEmpty(),
VlessEncryption = item.Security,
};
break;
case EConfigType.Hysteria2:
extra = extra with
{
SalamanderPass = item.Path.NullIfEmpty(),
Ports = item.Ports.NullIfEmpty(),
UpMbps = _config.HysteriaItem.UpMbps,
DownMbps = _config.HysteriaItem.DownMbps,
HopInterval = _config.HysteriaItem.HopInterval.ToString(),
};
break;
case EConfigType.TUIC:
item.Username = item.Id;
item.Id = item.Security;
item.Password = item.Security;
break;
case EConfigType.HTTP:
case EConfigType.SOCKS:
item.Username = item.Security;
break;
case EConfigType.WireGuard:
extra = extra with
{
WgPublicKey = item.PublicKey.NullIfEmpty(),
WgInterfaceAddress = item.RequestHost.NullIfEmpty(),
WgReserved = item.Path.NullIfEmpty(),
WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280
};
break;
}
item.SetProtocolExtra(extra);
item.Password = item.Id;
item.ConfigVersion = 3;
updateProfileItems.Add(item);
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtra Error: {ex}");
}
}
if (updateProfileItems.Count > 0)
{
try
{
var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems);
return count;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}");
return 0;
}
}
else
{
return 0;
}
}
private async Task<bool> MigrateProfileExtraGroup()
{
#pragma warning disable CS0618
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
var groupItems = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!));
var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 AND ConfigType IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain})";
var items = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql);
if (items is null || items.Count == 0)
{
Logging.SaveLog("MigrateProfileExtraGroup: No items to migrate.");
return true;
}
Logging.SaveLog($"MigrateProfileExtraGroup: Found {items.Count} group items to migrate.");
var updateProfileItems = new List<ProfileItem>();
foreach (var item in items)
{
try
{
var extra = item.GetProtocolExtra();
extra = extra with { GroupType = nameof(item.ConfigType) };
groupItems.TryGetValue(item.IndexId, out var groupItem);
if (groupItem != null && !groupItem.NotHasChild())
{
extra = extra with
{
ChildItems = groupItem.ChildItems,
SubChildItems = groupItem.SubChildItems,
Filter = groupItem.Filter,
MultipleLoad = groupItem.MultipleLoad,
};
}
item.SetProtocolExtra(extra);
item.ConfigVersion = 3;
updateProfileItems.Add(item);
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup item error [{item.IndexId}]: {ex}");
}
}
if (updateProfileItems.Count > 0)
{
try
{
var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems);
Logging.SaveLog($"MigrateProfileExtraGroup: Successfully updated {updateProfileItems.Count} items.");
return updateProfileItems.Count == count;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}");
return false;
}
}
return true;
//await ProfileGroupItemManager.Instance.ClearAll();
#pragma warning restore CS0618
}
#endregion SqliteHelper #endregion SqliteHelper
#region Core Type #region Core Type

View file

@ -12,7 +12,7 @@ public class GroupProfileManager
return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>()); return await HasCycle(indexId, extraInfo, new HashSet<string>(), new HashSet<string>());
} }
public static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack) private static async Task<bool> HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet<string> visited, HashSet<string> stack)
{ {
if (indexId.IsNullOrEmpty() || extraInfo == null) if (indexId.IsNullOrEmpty() || extraInfo == null)
{ {
@ -81,14 +81,15 @@ public class GroupProfileManager
{ {
return new(); return new();
} }
var items = await GetSelectedChildProfileItems(protocolExtra);
var subItems = await GetSubChildProfileItems(protocolExtra); var items = new List<ProfileItem>();
items.AddRange(subItems); items.AddRange(await GetSubChildProfileItems(protocolExtra));
items.AddRange(await GetSelectedChildProfileItems(protocolExtra));
return items; return items;
} }
public static async Task<List<ProfileItem>> GetSelectedChildProfileItems(ProtocolExtraItem? extra) private static async Task<List<ProfileItem>> GetSelectedChildProfileItems(ProtocolExtraItem? extra)
{ {
if (extra == null || extra.ChildItems.IsNullOrEmpty()) if (extra == null || extra.ChildItems.IsNullOrEmpty())
{ {
@ -105,10 +106,10 @@ public class GroupProfileManager
p.ConfigType != EConfigType.Custom p.ConfigType != EConfigType.Custom
) )
.ToList(); .ToList();
return childProfiles; return childProfiles ?? new();
} }
public static async Task<List<ProfileItem>> GetSubChildProfileItems(ProtocolExtraItem? extra) private static async Task<List<ProfileItem>> GetSubChildProfileItems(ProtocolExtraItem? extra)
{ {
if (extra == null || extra.SubChildItems.IsNullOrEmpty()) if (extra == null || extra.SubChildItems.IsNullOrEmpty())
{ {

View file

@ -1,7 +1,7 @@
namespace ServiceLib.Models; namespace ServiceLib.Models;
[Serializable] [Serializable]
public class ProfileItem : ReactiveObject public class ProfileItem
{ {
private ProtocolExtraItem? _protocolExtraCache; private ProtocolExtraItem? _protocolExtraCache;
@ -10,6 +10,7 @@ public class ProfileItem : ReactiveObject
IndexId = string.Empty; IndexId = string.Empty;
ConfigType = EConfigType.VMess; ConfigType = EConfigType.VMess;
ConfigVersion = 3; ConfigVersion = 3;
Subid = string.Empty;
Address = string.Empty; Address = string.Empty;
Port = 0; Port = 0;
Password = string.Empty; Password = string.Empty;
@ -21,7 +22,6 @@ public class ProfileItem : ReactiveObject
Path = string.Empty; Path = string.Empty;
StreamSecurity = string.Empty; StreamSecurity = string.Empty;
AllowInsecure = string.Empty; AllowInsecure = string.Empty;
Subid = string.Empty;
} }
#region function #region function
@ -148,26 +148,26 @@ public class ProfileItem : ReactiveObject
public string IndexId { get; set; } public string IndexId { get; set; }
public EConfigType ConfigType { get; set; } public EConfigType ConfigType { get; set; }
public ECoreType? CoreType { get; set; }
public int ConfigVersion { get; set; } public int ConfigVersion { get; set; }
public string Subid { get; set; }
public bool IsSub { get; set; } = true;
public int? PreSocksPort { get; set; }
public bool DisplayLog { get; set; } = true;
public string Remarks { get; set; }
public string Address { get; set; } public string Address { get; set; }
public int Port { get; set; } public int Port { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Network { get; set; } public string Network { get; set; }
public string Remarks { get; set; }
public string HeaderType { get; set; } public string HeaderType { get; set; }
public string RequestHost { get; set; } public string RequestHost { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string StreamSecurity { get; set; } public string StreamSecurity { get; set; }
public string AllowInsecure { get; set; } public string AllowInsecure { get; set; }
public string Subid { get; set; }
public bool IsSub { get; set; } = true;
public string Sni { get; set; } public string Sni { get; set; }
public string Alpn { get; set; } = string.Empty; public string Alpn { get; set; } = string.Empty;
public ECoreType? CoreType { get; set; }
public int? PreSocksPort { get; set; }
public string Fingerprint { get; set; } public string Fingerprint { get; set; }
public bool DisplayLog { get; set; } = true;
public string PublicKey { get; set; } public string PublicKey { get; set; }
public string ShortId { get; set; } public string ShortId { get; set; }
public string SpiderX { get; set; } public string SpiderX { get; set; }

View file

@ -1,16 +1,24 @@
namespace ServiceLib.Models; namespace ServiceLib.Models;
[Serializable] [Serializable]
public class ProfileItemModel : ProfileItem public class ProfileItemModel : ReactiveObject
{ {
public bool IsActive { get; set; } public bool IsActive { get; set; }
public string IndexId { get; set; }
public EConfigType ConfigType { get; set; }
public string Remarks { get; set; }
public string Address { get; set; }
public int Port { get; set; }
public string Network { get; set; }
public string StreamSecurity { get; set; }
public string Subid { get; set; }
public string SubRemarks { get; set; } public string SubRemarks { get; set; }
public int Sort { get; set; }
[Reactive] [Reactive]
public int Delay { get; set; } public int Delay { get; set; }
public decimal Speed { get; set; } public decimal Speed { get; set; }
public int Sort { get; set; }
[Reactive] [Reactive]
public string DelayVal { get; set; } public string DelayVal { get; set; }
@ -29,4 +37,15 @@ public class ProfileItemModel : ProfileItem
[Reactive] [Reactive]
public string TotalDown { get; set; } public string TotalDown { get; set; }
public string GetSummary()
{
var summary = $"[{ConfigType}] {Remarks}";
if (!ConfigType.IsComplexType())
{
summary += $"({Address}:{Port})";
}
return summary;
}
} }

View file

@ -1680,6 +1680,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Configuration item preview 的本地化字符串。
/// </summary>
public static string menuServerListPreview {
get {
return ResourceManager.GetString("menuServerListPreview", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Configuration 的本地化字符串。 /// 查找类似 Configuration 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -1665,4 +1665,7 @@
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>端口跳跃间隔</value> <value>端口跳跃间隔</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>子配置项预览</value>
</data>
</root> </root>

View file

@ -1665,4 +1665,7 @@
<data name="TbHopInt7" xml:space="preserve"> <data name="TbHopInt7" xml:space="preserve">
<value>Port hopping interval</value> <value>Port hopping interval</value>
</data> </data>
<data name="menuServerListPreview" xml:space="preserve">
<value>Configuration item preview</value>
</data>
</root> </root>

View file

@ -27,6 +27,8 @@ public class AddGroupServerViewModel : MyReactiveObject
public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; } //public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; } public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
@ -182,6 +184,32 @@ public class AddGroupServerViewModel : MyReactiveObject
await Task.CompletedTask; await Task.CompletedTask;
} }
private ProtocolExtraItem GetUpdatedProtocolExtra()
{
return SelectedSource.GetProtocolExtra() with
{
ChildItems =
Utils.List2String(ChildItemsObs.Where(s => !s.IndexId.IsNullOrEmpty()).Select(s => s.IndexId).ToList()),
MultipleLoad = PolicyGroupType switch
{
var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing,
var s when s == ResUI.TbFallback => EMultipleLoad.Fallback,
var s when s == ResUI.TbRandom => EMultipleLoad.Random,
var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin,
var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad,
_ => EMultipleLoad.LeastPing,
},
SubChildItems = SelectedSubItem?.Id,
Filter = Filter,
};
}
public async Task UpdatePreviewList()
{
AllProfilePreviewItemsObs.Clear();
AllProfilePreviewItemsObs.AddRange(await GroupProfileManager.GetChildProfileItemsByProtocolExtra(GetUpdatedProtocolExtra()));
}
private async Task SaveServerAsync() private async Task SaveServerAsync()
{ {
var remarks = SelectedSource.Remarks; var remarks = SelectedSource.Remarks;
@ -202,24 +230,11 @@ public class AddGroupServerViewModel : MyReactiveObject
return; return;
} }
SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with var protocolExtra = GetUpdatedProtocolExtra();
{
ChildItems =
Utils.List2String(ChildItemsObs.Where(s => !s.IndexId.IsNullOrEmpty()).Select(s => s.IndexId).ToList()),
MultipleLoad = PolicyGroupType switch
{
var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing,
var s when s == ResUI.TbFallback => EMultipleLoad.Fallback,
var s when s == ResUI.TbRandom => EMultipleLoad.Random,
var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin,
var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad,
_ => EMultipleLoad.LeastPing,
},
SubChildItems = SelectedSubItem?.Id,
Filter = Filter,
});
var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, SelectedSource.GetProtocolExtra()); SelectedSource.SetProtocolExtra(protocolExtra);
var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra);
if (hasCycle) if (hasCycle)
{ {
NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks));

View file

@ -49,12 +49,15 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public string WgPublicKey { get; set; } public string WgPublicKey { get; set; }
//[Reactive] //[Reactive]
//public string WgPresharedKey { get; set; } //public string WgPresharedKey { get; set; }
[Reactive] [Reactive]
public string WgInterfaceAddress { get; set; } public string WgInterfaceAddress { get; set; }
[Reactive] [Reactive]
public string WgReserved { get; set; } public string WgReserved { get; set; }
[Reactive] [Reactive]
public int WgMtu { get; set; } public int WgMtu { get; set; }

View file

@ -200,7 +200,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter);
lstModel = (from t in lstModel lstModel = (from t in lstModel
select new ProfileItemModel select new ProfileItemModel
{ {

View file

@ -428,7 +428,7 @@ public class ProfilesViewModel : MyReactiveObject
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter);
await ConfigHandler.SetDefaultServer(_config, lstModel); await ConfigHandler.SetDefaultServer(_config, lstModel);

View file

@ -303,7 +303,7 @@ public class StatusBarViewModel : MyReactiveObject
private async Task RefreshServersMenu() private async Task RefreshServersMenu()
{ {
var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, ""); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, "");
Servers.Clear(); Servers.Clear();
if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit)
@ -315,7 +315,7 @@ public class StatusBarViewModel : MyReactiveObject
BlServers = true; BlServers = true;
for (var k = 0; k < lstModel.Count; k++) for (var k = 0; k < lstModel.Count; k++)
{ {
ProfileItem it = lstModel[k]; var it = lstModel[k];
var name = it.GetSummary(); var name = it.GetSummary();
var item = new ComboItem() { ID = it.IndexId, Text = name }; var item = new ComboItem() { ID = it.IndexId, Text = name };

View file

@ -88,7 +88,10 @@
</Grid> </Grid>
</Grid> </Grid>
<TabControl HorizontalContentAlignment="Stretch" DockPanel.Dock="Top"> <TabControl
x:Name="tabControl"
HorizontalContentAlignment="Stretch"
DockPanel.Dock="Top">
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}"> <TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList}">
<Grid <Grid
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
@ -134,7 +137,6 @@
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList2}"> <TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerList2}">
<DataGrid <DataGrid
x:Name="lstChild" x:Name="lstChild"
Grid.Row="1"
AutoGenerateColumns="False" AutoGenerateColumns="False"
Background="Transparent" Background="Transparent"
BorderThickness="1" BorderThickness="1"
@ -204,6 +206,48 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</TabItem> </TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerListPreview}">
<DataGrid
x:Name="lstPreviewChild"
AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="1"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="False"
GridLinesVisibility="All"
HeadersVisibility="Column"
IsReadOnly="True"
ItemsSource="{Binding AllProfilePreviewItemsObs}"
SelectionMode="Extended">
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</Window> </Window>

View file

@ -16,6 +16,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
Loaded += Window_Loaded; Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
lstChild.SelectionChanged += LstChild_SelectionChanged; lstChild.SelectionChanged += LstChild_SelectionChanged;
tabControl.SelectionChanged += TabControl_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
@ -38,6 +39,10 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
case EConfigType.ProxyChain: case EConfigType.ProxyChain:
Title = ResUI.TbConfigTypeProxyChain; Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.IsVisible = false; gridPolicyGroup.IsVisible = false;
if (tabControl.Items.Count > 0)
{
tabControl.Items.RemoveAt(0);
}
break; break;
} }
@ -50,7 +55,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
@ -167,4 +171,29 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList(); ViewModel.SelectedChildren = lstChild.SelectedItems.Cast<ProfileItem>().ToList();
} }
} }
private async void TabControl_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
try
{
if (e.Source is not TabControl tc)
{
return;
}
if (!(tc.SelectedIndex == tc.Items.Count - 1 && tc.Items.Count > 0))
{
return;
}
if (ViewModel == null)
{
return;
}
await ViewModel.UpdatePreviewList();
}
catch
{
// ignored
}
}
} }

View file

@ -135,6 +135,7 @@
</Grid> </Grid>
<TabControl <TabControl
x:Name="tabControl"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
DockPanel.Dock="Top"> DockPanel.Dock="Top">
@ -272,6 +273,47 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</TabItem> </TabItem>
<TabItem HorizontalAlignment="Left" Header="{x:Static resx:ResUI.menuServerListPreview}">
<DataGrid
x:Name="lstPreviewChild"
AutoGenerateColumns="False"
BorderThickness="1"
CanUserAddRows="False"
CanUserResizeRows="False"
CanUserSortColumns="False"
EnableRowVirtualization="True"
GridLinesVisibility="All"
HeadersVisibility="Column"
IsReadOnly="True"
Style="{StaticResource DefDataGrid}">
<DataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding ConfigType}"
Header="{x:Static resx:ResUI.LvServiceType}" />
<DataGridTextColumn
Width="150"
Binding="{Binding Remarks}"
Header="{x:Static resx:ResUI.LvRemarks}" />
<DataGridTextColumn
Width="120"
Binding="{Binding Address}"
Header="{x:Static resx:ResUI.LvAddress}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Port}"
Header="{x:Static resx:ResUI.LvPort}" />
<DataGridTextColumn
Width="100"
Binding="{Binding Network}"
Header="{x:Static resx:ResUI.LvTransportProtocol}" />
<DataGridTextColumn
Width="100"
Binding="{Binding StreamSecurity}"
Header="{x:Static resx:ResUI.LvTLS}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</base:WindowBase> </base:WindowBase>

View file

@ -11,6 +11,7 @@ public partial class AddGroupServerWindow
PreviewKeyDown += AddGroupServerWindow_PreviewKeyDown; PreviewKeyDown += AddGroupServerWindow_PreviewKeyDown;
lstChild.SelectionChanged += LstChild_SelectionChanged; lstChild.SelectionChanged += LstChild_SelectionChanged;
menuSelectAllChild.Click += MenuSelectAllChild_Click; menuSelectAllChild.Click += MenuSelectAllChild_Click;
tabControl.SelectionChanged += TabControl_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
@ -33,6 +34,10 @@ public partial class AddGroupServerWindow
case EConfigType.ProxyChain: case EConfigType.ProxyChain:
Title = ResUI.TbConfigTypeProxyChain; Title = ResUI.TbConfigTypeProxyChain;
gridPolicyGroup.Visibility = Visibility.Collapsed; gridPolicyGroup.Visibility = Visibility.Collapsed;
if (tabControl.Items.Count > 0)
{
tabControl.Items.RemoveAt(0);
}
break; break;
} }
@ -48,6 +53,8 @@ public partial class AddGroupServerWindow
this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.AllProfilePreviewItemsObs, v => v.lstPreviewChild.ItemsSource).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
@ -148,4 +155,29 @@ public partial class AddGroupServerWindow
{ {
lstChild.SelectAll(); lstChild.SelectAll();
} }
private async void TabControl_SelectionChanged(object? sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (e.Source is not System.Windows.Controls.TabControl tc)
{
return;
}
if (!(tc.SelectedIndex == tc.Items.Count - 1 && tc.Items.Count > 0))
{
return;
}
if (ViewModel == null)
{
return;
}
await ViewModel.UpdatePreviewList();
}
catch
{
// ignored
}
}
} }