mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-30 01:34:08 +00:00
Inner uri import and export
This commit is contained in:
parent
d13f7a4db6
commit
737da7d40c
16 changed files with 343 additions and 0 deletions
|
|
@ -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,53 @@ 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.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),
|
||||||
|
_ => -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 +1862,11 @@ public static class ConfigHandler
|
||||||
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
|
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (counter < 1)
|
||||||
|
{
|
||||||
|
counter = await AddBatchServers4InnerUri(config, strData, subid, isSub);
|
||||||
|
}
|
||||||
|
|
||||||
//maybe other sub
|
//maybe other sub
|
||||||
if (counter < 1)
|
if (counter < 1)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
222
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
222
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (profileItem.ConfigVersion != 4)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
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("=", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -1041,6 +1041,15 @@ namespace ServiceLib.Resx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Backup to Clipboard 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuExport4Backup {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuExport4Backup", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Export 的本地化字符串。
|
/// 查找类似 Export 的本地化字符串。
|
||||||
/// </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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>备份至剪贴板 (多选)</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="menuExport4Backup" xml:space="preserve">
|
||||||
|
<value>Backup 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="menuExport4Backup" Header="{x:Static resx:ResUI.menuExport4Backup}" />
|
||||||
</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.menuExport4Backup).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="menuExport4Backup"
|
||||||
|
Height="{StaticResource MenuItemHeight}"
|
||||||
|
Header="{x:Static resx:ResUI.menuExport4Backup}" />
|
||||||
</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.menuExport4Backup).DisposeWith(disposables);
|
||||||
|
|
||||||
AppEvents.AppExitRequested
|
AppEvents.AppExitRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue