mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-26 07:53:49 +00:00
Inner uri (#9245)
Some checks are pending
release Linux / build (push) Waiting to run
release Linux / release-zip (push) Blocked by required conditions
release Linux / build and release deb x64 & arm64 (push) Waiting to run
release Linux / build and release rpm x64 & arm64 (push) Waiting to run
release Linux / build and release rpm riscv64 (push) Waiting to run
release macOS / build (push) Waiting to run
release macOS / release-zip (push) Blocked by required conditions
release macOS / package and release macOS dmg (push) Blocked by required conditions
release Windows desktop (Avalonia UI) / build (push) Waiting to run
release Windows desktop (Avalonia UI) / release-zip (push) Blocked by required conditions
release Windows / build (push) Waiting to run
release Windows / release-zip (push) Blocked by required conditions
Some checks are pending
release Linux / build (push) Waiting to run
release Linux / release-zip (push) Blocked by required conditions
release Linux / build and release deb x64 & arm64 (push) Waiting to run
release Linux / build and release rpm x64 & arm64 (push) Waiting to run
release Linux / build and release rpm riscv64 (push) Waiting to run
release macOS / build (push) Waiting to run
release macOS / release-zip (push) Blocked by required conditions
release macOS / package and release macOS dmg (push) Blocked by required conditions
release Windows desktop (Avalonia UI) / build (push) Waiting to run
release Windows desktop (Avalonia UI) / release-zip (push) Blocked by required conditions
release Windows / build (push) Waiting to run
release Windows / release-zip (push) Blocked by required conditions
* Inner uri import and export * Add tests * Fix * Compress export length
This commit is contained in:
parent
d13f7a4db6
commit
75ea81dd69
17 changed files with 468 additions and 0 deletions
37
v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs
Normal file
37
v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
using AwesomeAssertions;
|
||||||
|
using ServiceLib.Enums;
|
||||||
|
using ServiceLib.Handler.Fmt;
|
||||||
|
using ServiceLib.Tests.CoreConfig;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ServiceLib.Tests.Fmt;
|
||||||
|
|
||||||
|
public class InnerFmtTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
|
||||||
|
{
|
||||||
|
var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a");
|
||||||
|
var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b");
|
||||||
|
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "group-1", "group-1",
|
||||||
|
[childA.IndexId, childB.IndexId]);
|
||||||
|
group.SetProtocolExtra(group.GetProtocolExtra() with { SubChildItems = "original-sub" });
|
||||||
|
|
||||||
|
var uri = InnerFmt.ToUri([group, childA, childB]);
|
||||||
|
|
||||||
|
uri.Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
var resolved = InnerFmt.Resolve(uri!, "sub-123");
|
||||||
|
|
||||||
|
resolved.Should().NotBeNull();
|
||||||
|
resolved.Should().HaveCount(3);
|
||||||
|
|
||||||
|
var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks);
|
||||||
|
var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks);
|
||||||
|
var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks);
|
||||||
|
|
||||||
|
resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup);
|
||||||
|
resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123");
|
||||||
|
resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,7 @@ public class Global
|
||||||
public const string HttpsProtocol = "https://";
|
public const string HttpsProtocol = "https://";
|
||||||
public const string SocksProtocol = "socks://";
|
public const string SocksProtocol = "socks://";
|
||||||
public const string Socks5Protocol = "socks5://";
|
public const string Socks5Protocol = "socks5://";
|
||||||
|
public const string InnerUriProtocol = "v2rayn://";
|
||||||
public const string AsIs = "AsIs";
|
public const string AsIs = "AsIs";
|
||||||
public const string IPIfNonMatch = "IPIfNonMatch";
|
public const string IPIfNonMatch = "IPIfNonMatch";
|
||||||
public const string IPOnDemand = "IPOnDemand";
|
public const string IPOnDemand = "IPOnDemand";
|
||||||
|
|
|
||||||
|
|
@ -1767,6 +1767,55 @@ public static class ConfigHandler
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<int> AddBatchServers4InnerUri(Config config, string strData, string subid, bool isSub)
|
||||||
|
{
|
||||||
|
if (strData.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSub && subid.IsNotEmpty())
|
||||||
|
{
|
||||||
|
await RemoveServersViaSubid(config, subid, isSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lstServer = InnerFmt.Resolve(strData, subid);
|
||||||
|
if (lstServer?.Count > 0)
|
||||||
|
{
|
||||||
|
var counter = 0;
|
||||||
|
foreach (var profileItem in lstServer)
|
||||||
|
{
|
||||||
|
profileItem.Subid = subid;
|
||||||
|
profileItem.IsSub = isSub;
|
||||||
|
|
||||||
|
var addStatus = profileItem.ConfigType switch
|
||||||
|
{
|
||||||
|
EConfigType.VMess => await AddVMessServer(config, profileItem),
|
||||||
|
EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem),
|
||||||
|
EConfigType.HTTP => await AddHttpServer(config, profileItem),
|
||||||
|
EConfigType.SOCKS => await AddSocksServer(config, profileItem),
|
||||||
|
EConfigType.Trojan => await AddTrojanServer(config, profileItem),
|
||||||
|
EConfigType.VLESS => await AddVlessServer(config, profileItem),
|
||||||
|
EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem),
|
||||||
|
EConfigType.TUIC => await AddTuicServer(config, profileItem),
|
||||||
|
EConfigType.WireGuard => await AddWireguardServer(config, profileItem),
|
||||||
|
EConfigType.Anytls => await AddAnytlsServer(config, profileItem),
|
||||||
|
EConfigType.Naive => await AddNaiveServer(config, profileItem),
|
||||||
|
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem),
|
||||||
|
_ => -1,
|
||||||
|
};
|
||||||
|
if (addStatus == 0)
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await SaveConfig(config);
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main entry point for adding batch servers from various formats
|
/// Main entry point for adding batch servers from various formats
|
||||||
/// Tries different parsing methods to import as many servers as possible
|
/// Tries different parsing methods to import as many servers as possible
|
||||||
|
|
@ -1815,6 +1864,20 @@ public static class ConfigHandler
|
||||||
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
|
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//May be standard uri mixed with internal uri
|
||||||
|
var innerUriCount = await AddBatchServers4InnerUri(config, strData, subid, isSub);
|
||||||
|
if (innerUriCount > 0)
|
||||||
|
{
|
||||||
|
if (counter > 0)
|
||||||
|
{
|
||||||
|
counter += innerUriCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
counter = innerUriCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//maybe other sub
|
//maybe other sub
|
||||||
if (counter < 1)
|
if (counter < 1)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
299
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
299
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
namespace ServiceLib.Handler.Fmt;
|
||||||
|
|
||||||
|
public class InnerFmt
|
||||||
|
{
|
||||||
|
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
||||||
|
|
||||||
|
public static List<ProfileItem>? Resolve(string strData, string subid)
|
||||||
|
{
|
||||||
|
var list = new List<ProfileItem>();
|
||||||
|
// Overwrite externally imported indexIds to avoid possible sources of attacks
|
||||||
|
var indexIdMap = new Dictionary<string, string>();
|
||||||
|
using (var reader = new StringReader(strData))
|
||||||
|
{
|
||||||
|
while (reader.ReadLine() is { } line)
|
||||||
|
{
|
||||||
|
if (line.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var trimmedLine = line.Trim();
|
||||||
|
if (!line.StartsWith(Global.InnerUriProtocol, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var profileItem = ResolveSingle(trimmedLine);
|
||||||
|
if (profileItem is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (profileItem.ConfigType == EConfigType.Custom)
|
||||||
|
{
|
||||||
|
// Unsupported, also to avoid possible sources of attacks, skip it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// overwrite indexId
|
||||||
|
var newIndexId = Utils.GetGuid(false);
|
||||||
|
if (!profileItem.IndexId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
// Ignore duplicated indexId
|
||||||
|
indexIdMap[profileItem.IndexId] = newIndexId;
|
||||||
|
}
|
||||||
|
profileItem.IndexId = newIndexId;
|
||||||
|
list.Add(profileItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For group-type profile items, also overwrite the ChildItems and ChildSubId
|
||||||
|
var emptyGroupProfileList = new List<ProfileItem>();
|
||||||
|
foreach (var item in list.Where(i => i.ConfigType.IsGroupType()))
|
||||||
|
{
|
||||||
|
var protocolExtra = item.GetProtocolExtra();
|
||||||
|
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
|
||||||
|
// which means it will be replaced with the subid, otherwise set it to null
|
||||||
|
//if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||||
|
if (protocolExtra.SubChildItems == "self")
|
||||||
|
{
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
SubChildItems = subid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
SubChildItems = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||||
|
{
|
||||||
|
var newChildIndexIds = childIndexIds
|
||||||
|
.Select(id => indexIdMap.GetValueOrDefault(id, null))
|
||||||
|
.Where(id => !id.IsNullOrEmpty())
|
||||||
|
.ToList();
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
ChildItems = Utils.List2String(newChildIndexIds)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
ChildItems = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.SetProtocolExtra(protocolExtra);
|
||||||
|
if (protocolExtra.SubChildItems.IsNullOrEmpty()
|
||||||
|
&& protocolExtra.ChildItems.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
emptyGroupProfileList.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove empty group profile items
|
||||||
|
list.RemoveAll(emptyGroupProfileList.Contains);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ToUri(List<ProfileItem> items)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.ConfigType == EConfigType.Custom)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var itemClone = JsonUtils.DeepCopy(item);
|
||||||
|
if (itemClone is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// overwrite indexId
|
||||||
|
var originalIndexId = itemClone.IndexId;
|
||||||
|
var newIndexId = GetReproducibleExportId(originalIndexId);
|
||||||
|
itemClone.IndexId = newIndexId;
|
||||||
|
if (itemClone.ConfigType.IsGroupType())
|
||||||
|
{
|
||||||
|
var protocolExtra = itemClone.GetProtocolExtra();
|
||||||
|
if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
SubChildItems = "self"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||||
|
{
|
||||||
|
var newChildIndexIds = childIndexIds
|
||||||
|
.Select(GetReproducibleExportId)
|
||||||
|
.Where(id => !id.IsNullOrEmpty())
|
||||||
|
.ToList();
|
||||||
|
protocolExtra = protocolExtra with
|
||||||
|
{
|
||||||
|
ChildItems = Utils.List2String(newChildIndexIds)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
itemClone.SetProtocolExtra(protocolExtra);
|
||||||
|
}
|
||||||
|
var uri = ToUriSingle(itemClone);
|
||||||
|
if (!uri.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
sb.AppendLine(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.Length > 0 ? sb.ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileItem? ResolveSingle(string str)
|
||||||
|
{
|
||||||
|
// format: v2rayn://vless/{url-safe base64 encoded_string}
|
||||||
|
var parsedUri = Utils.TryUri(str);
|
||||||
|
if (parsedUri is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var segment = parsedUri.AbsolutePath.TrimStart('/');
|
||||||
|
var decodedResult = Utils.Base64Decode(segment);
|
||||||
|
var jsonNode = JsonUtils.ParseJson(decodedResult);
|
||||||
|
if (jsonNode is not JsonObject jsonObj)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// flatten
|
||||||
|
// move jsonObj.ProtoExtraObj to jsonObj.ProtoExtra (string)
|
||||||
|
// move jsonObj.TransportExtraObj to jsonObj.TransportExtra (string)
|
||||||
|
if (jsonObj.TryGetPropertyValue("ProtoExtraObj", out var protoExtraNode)
|
||||||
|
&& protoExtraNode is JsonObject protoExtraObj)
|
||||||
|
{
|
||||||
|
jsonObj["ProtoExtra"] = JsonUtils.Serialize(protoExtraObj, false);
|
||||||
|
jsonObj.Remove("ProtoExtraObj");
|
||||||
|
}
|
||||||
|
if (jsonObj.TryGetPropertyValue("TransportExtraObj", out var transportExtraNode)
|
||||||
|
&& transportExtraNode is JsonObject transportExtraObj)
|
||||||
|
{
|
||||||
|
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
|
||||||
|
jsonObj.Remove("TransportExtraObj");
|
||||||
|
}
|
||||||
|
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
|
||||||
|
if (profileItem is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (profileItem.ConfigVersion != 4)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Check Enum.IsDefined
|
||||||
|
if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (profileItem.CoreType is not (null or ECoreType.Xray or ECoreType.sing_box))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var protocolExtra = profileItem.GetProtocolExtra();
|
||||||
|
var multipleLoad = protocolExtra.MultipleLoad;
|
||||||
|
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return profileItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ToUriSingle(ProfileItem item)
|
||||||
|
{
|
||||||
|
var jsonNode = JsonUtils.ParseJson(JsonUtils.Serialize(item, false));
|
||||||
|
if (jsonNode is not JsonObject jsonObj)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// unflatten
|
||||||
|
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
|
||||||
|
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
|
||||||
|
if (jsonObj.TryGetPropertyValue("ProtoExtra", out var protoExtraNode)
|
||||||
|
&& protoExtraNode is JsonValue protoExtraValue
|
||||||
|
&& protoExtraValue.TryGetValue<string>(out var protoExtraStr)
|
||||||
|
&& !protoExtraStr.IsNullOrEmpty()
|
||||||
|
&& JsonUtils.ParseJson(protoExtraStr) is JsonObject protoExtraObj)
|
||||||
|
{
|
||||||
|
jsonObj["ProtoExtraObj"] = protoExtraObj;
|
||||||
|
jsonObj.Remove("ProtoExtra");
|
||||||
|
}
|
||||||
|
if (jsonObj.TryGetPropertyValue("TransportExtra", out var transportExtraNode)
|
||||||
|
&& transportExtraNode is JsonValue transportExtraValue
|
||||||
|
&& transportExtraValue.TryGetValue<string>(out var transportExtraStr)
|
||||||
|
&& !transportExtraStr.IsNullOrEmpty()
|
||||||
|
&& JsonUtils.ParseJson(transportExtraStr) is JsonObject transportExtraObj)
|
||||||
|
{
|
||||||
|
jsonObj["TransportExtraObj"] = transportExtraObj;
|
||||||
|
jsonObj.Remove("TransportExtra");
|
||||||
|
}
|
||||||
|
// Remove empty properties to reduce the length of the exported string
|
||||||
|
RemoveEmptyJson(jsonObj);
|
||||||
|
var jsonStr = JsonUtils.Serialize(jsonObj, false);
|
||||||
|
var encodedStr = Utils.Base64Encode(jsonStr).Replace('+', '-').Replace('/', '_').Replace("=", "");
|
||||||
|
return $"{Global.InnerUriProtocol}{item.ConfigType.ToString().ToLower()}/{encodedStr}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetReproducibleExportId(string originalIndexId)
|
||||||
|
{
|
||||||
|
if (originalIndexId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return originalIndexId;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = HashCode.Combine(SessionSalt.Value, originalIndexId) & 0x7FFFFFFF;
|
||||||
|
var bytes = BitConverter.GetBytes(hash);
|
||||||
|
return Convert.ToBase64String(bytes).Replace("=", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveEmptyJson(JsonNode? node)
|
||||||
|
{
|
||||||
|
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
||||||
|
if (node is JsonObject jsonObject)
|
||||||
|
{
|
||||||
|
var propertiesToRemove = new List<string>();
|
||||||
|
|
||||||
|
foreach (var property in jsonObject)
|
||||||
|
{
|
||||||
|
RemoveEmptyJson(property.Value);
|
||||||
|
|
||||||
|
if (IsEmpty(property.Value))
|
||||||
|
{
|
||||||
|
propertiesToRemove.Add(property.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in propertiesToRemove)
|
||||||
|
{
|
||||||
|
jsonObject.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (node is JsonArray jsonArray)
|
||||||
|
{
|
||||||
|
for (var i = jsonArray.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
RemoveEmptyJson(jsonArray[i]);
|
||||||
|
|
||||||
|
if (IsEmpty(jsonArray[i]))
|
||||||
|
{
|
||||||
|
jsonArray.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEmpty(JsonNode? node)
|
||||||
|
{
|
||||||
|
return node switch
|
||||||
|
{
|
||||||
|
null => true,
|
||||||
|
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
|
||||||
|
JsonObject obj => obj.Count == 0,
|
||||||
|
JsonArray arr => arr.Count == 0,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -1023,6 +1023,15 @@ namespace ServiceLib.Resx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Export v2rayN Internal Share Link to Clipboard 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuExport2InnerUri {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuExport2InnerUri", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Export Share Link to Clipboard 的本地化字符串。
|
/// 查找类似 Export Share Link to Clipboard 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1725,4 +1725,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1728,4 +1728,7 @@
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1725,4 +1725,7 @@
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>导出 v2rayN 内部分享链接至剪贴板 (多选)</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1725,4 +1725,7 @@
|
||||||
<data name="TbPreSharedKey" xml:space="preserve">
|
<data name="TbPreSharedKey" xml:space="preserve">
|
||||||
<value>PreSharedKey</value>
|
<value>PreSharedKey</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||||
|
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -72,6 +72,7 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
public ReactiveCommand<Unit, Unit> Export2ClientConfigClipboardCmd { get; }
|
public ReactiveCommand<Unit, Unit> Export2ClientConfigClipboardCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlCmd { get; }
|
public ReactiveCommand<Unit, Unit> Export2ShareUrlCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlBase64Cmd { get; }
|
public ReactiveCommand<Unit, Unit> Export2ShareUrlBase64Cmd { get; }
|
||||||
|
public ReactiveCommand<Unit, Unit> Export2InnerUriCmd { get; }
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> AddSubCmd { get; }
|
public ReactiveCommand<Unit, Unit> AddSubCmd { get; }
|
||||||
public ReactiveCommand<Unit, Unit> EditSubCmd { get; }
|
public ReactiveCommand<Unit, Unit> EditSubCmd { get; }
|
||||||
|
|
@ -212,6 +213,10 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
await Export2ShareUrlAsync(true);
|
await Export2ShareUrlAsync(true);
|
||||||
}, canEditRemove);
|
}, canEditRemove);
|
||||||
|
Export2InnerUriCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await Export2InnerUrlAsync();
|
||||||
|
}, canEditRemove);
|
||||||
|
|
||||||
//Subscription
|
//Subscription
|
||||||
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
|
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
|
@ -840,6 +845,32 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Export2InnerUrlAsync()
|
||||||
|
{
|
||||||
|
var lstSelected = await GetProfileItems(true);
|
||||||
|
if (lstSelected == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = string.Empty;
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
result = InnerFmt.ToUri(lstSelected);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
await _updateView?.Invoke(EViewAction.SetClipboardData, result);
|
||||||
|
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Add Servers
|
#endregion Add Servers
|
||||||
|
|
||||||
#region Subscription
|
#region Subscription
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@
|
||||||
Header="{x:Static resx:ResUI.menuExport2ShareUrl}"
|
Header="{x:Static resx:ResUI.menuExport2ShareUrl}"
|
||||||
InputGesture="Ctrl+C" />
|
InputGesture="Ctrl+C" />
|
||||||
<MenuItem x:Name="menuExport2ShareUrlBase64" Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
<MenuItem x:Name="menuExport2ShareUrlBase64" Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||||
|
<MenuItem x:Name="menuExport2InnerUri" Header="{x:Static resx:ResUI.menuExport2InnerUri}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||||
|
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables);
|
||||||
|
|
||||||
AppEvents.AppExitRequested
|
AppEvents.AppExitRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,10 @@
|
||||||
x:Name="menuExport2ShareUrlBase64"
|
x:Name="menuExport2ShareUrlBase64"
|
||||||
Height="{StaticResource MenuItemHeight}"
|
Height="{StaticResource MenuItemHeight}"
|
||||||
Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||||
|
<MenuItem
|
||||||
|
x:Name="menuExport2InnerUri"
|
||||||
|
Height="{StaticResource MenuItemHeight}"
|
||||||
|
Header="{x:Static resx:ResUI.menuExport2InnerUri}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ public partial class ProfilesView
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||||
|
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables);
|
||||||
|
|
||||||
AppEvents.AppExitRequested
|
AppEvents.AppExitRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue