Compare commits
2
Commits
@@ -12,8 +12,9 @@ public static class FlagEmojiParser
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a string and returns a list of ISO 3166-1 alpha-2 country codes found as emojis.
|
/// Parses a string and returns a list of ISO 3166-1 alpha-2 country codes found as emojis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<string> ExtractCountryCodes(string text)
|
public static List<string> ExtractCountryCodes(string text, out List<string> countryIndicators)
|
||||||
{
|
{
|
||||||
|
countryIndicators = [];
|
||||||
var countryCodes = new List<string>();
|
var countryCodes = new List<string>();
|
||||||
if (string.IsNullOrEmpty(text)) return countryCodes;
|
if (string.IsNullOrEmpty(text)) return countryCodes;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ public static class FlagEmojiParser
|
|||||||
var c2 = (char)('A' + letterOffset);
|
var c2 = (char)('A' + letterOffset);
|
||||||
|
|
||||||
countryCodes.Add($"{c1}{c2}");
|
countryCodes.Add($"{c1}{c2}");
|
||||||
|
countryIndicators.Add($"{char.ConvertFromUtf32(RegionalIndicatorStart + firstLetterCode.Value)}{char.ConvertFromUtf32(RegionalIndicatorStart + letterOffset)}");
|
||||||
|
|
||||||
// Reset to look for the next flag
|
// Reset to look for the next flag
|
||||||
firstLetterCode = null;
|
firstLetterCode = null;
|
||||||
@@ -59,4 +61,9 @@ public static class FlagEmojiParser
|
|||||||
|
|
||||||
return countryCodes;
|
return countryCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<string> ExtractCountryCodes(string text)
|
||||||
|
{
|
||||||
|
return ExtractCountryCodes(text, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -34,4 +34,19 @@ public abstract class Server
|
|||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = string.Empty;
|
} = string.Empty;
|
||||||
|
|
||||||
|
public bool IsDeleted
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
|
public bool IsDisabled
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
|
public List<string> Tags
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,29 @@ public class Subscription
|
|||||||
get; set;
|
get; set;
|
||||||
} = string.Empty;
|
} = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
public List<SubscriptionServer> Servers
|
public List<SubscriptionServer> Servers
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
} = [];
|
} = [];
|
||||||
|
|
||||||
|
public long Upload
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = 0;
|
||||||
|
|
||||||
|
public long Download
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = 0;
|
||||||
|
|
||||||
|
public long Total
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = 0;
|
||||||
|
|
||||||
|
public DateTime ExpireDate
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = DateTime.MaxValue;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using WinWARP.Core.Helper;
|
using WinWARP.Core.Helper;
|
||||||
using WinWARP.Core.Models.Entities;
|
using WinWARP.Core.Models.Entities;
|
||||||
using WinWARP.Core.Services.Interfaces;
|
using WinWARP.Core.Services.Interfaces;
|
||||||
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
|
||||||
|
|
||||||
namespace WinWARP.Core.Services.Implementations;
|
namespace WinWARP.Core.Services.Implementations;
|
||||||
|
|
||||||
public class SubscriptionFetcher : ISubscriptionFetcher
|
public partial class SubscriptionFetcher(IDialogService dialogService) : ISubscriptionFetcher
|
||||||
{
|
{
|
||||||
|
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) };
|
||||||
|
private readonly IDialogService _dialogService = dialogService;
|
||||||
|
|
||||||
public bool IsUrlValid(string subscriptionUrl)
|
public bool IsUrlValid(string subscriptionUrl)
|
||||||
{
|
{
|
||||||
Uri? validatedUri;
|
Uri? validatedUri;
|
||||||
@@ -23,13 +31,334 @@ public class SubscriptionFetcher : ISubscriptionFetcher
|
|||||||
|
|
||||||
public async Task<Subscription> FetchAsync(string subscriptionUrl)
|
public async Task<Subscription> FetchAsync(string subscriptionUrl)
|
||||||
{
|
{
|
||||||
// TODO: Implement the actual fetching logic here, e.g., using HttpClient to get the subscription data from the URL.
|
var subscription = new Subscription
|
||||||
await Task.Delay(10); // Simulate an asynchronous operation
|
|
||||||
return new Subscription
|
|
||||||
{
|
{
|
||||||
DefaultTitle = "Default Subscription",
|
Link = subscriptionUrl
|
||||||
Link = subscriptionUrl,
|
};
|
||||||
Servers = [new() { DefaultTitle = "Default Server", Region = "NO", Title = "Not default title" }]
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.GetAsync(subscriptionUrl);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
response.Headers.TryGetValues("Profile-Web-Page-Url", out var profileWebPage);
|
||||||
|
subscription.WebPage = profileWebPage?.FirstOrDefault() ?? string.Empty;
|
||||||
|
|
||||||
|
response.Headers.TryGetValues("Profile-Title", out var profileTitle);
|
||||||
|
subscription.DefaultTitle = GetSubscriptionTitle(profileTitle?.FirstOrDefault());
|
||||||
|
|
||||||
|
response.Headers.TryGetValues("Profile-Update-Interval", out var profileUpdateInterval);
|
||||||
|
if (profileUpdateInterval != null && int.TryParse(profileUpdateInterval.FirstOrDefault(), out var updateInterval)) subscription.DefaultUpdateInterval = updateInterval;
|
||||||
|
|
||||||
|
response.Headers.TryGetValues("Profile-Support-Url", out var profileSupportUrl);
|
||||||
|
subscription.SupportURL = profileSupportUrl?.FirstOrDefault() ?? string.Empty;
|
||||||
|
|
||||||
|
response.Headers.TryGetValues("Subscription-Userinfo", out var profileNetworkData);
|
||||||
|
var (Upload, Download, Total, Expire) = ParseNetworkData(profileNetworkData?.FirstOrDefault() ?? string.Empty);
|
||||||
|
subscription.Upload = Upload;
|
||||||
|
subscription.Download = Download;
|
||||||
|
subscription.Total = Total;
|
||||||
|
subscription.ExpireDate = Expire;
|
||||||
|
|
||||||
|
subscription.Servers = ParseServerData(await response.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
_dialogService.ShowErrorAsync(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Titles
|
||||||
|
private static string GetSubscriptionTitle(string title)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
return "New subscription";
|
||||||
|
}
|
||||||
|
else if (title.StartsWith("base64:"))
|
||||||
|
{
|
||||||
|
var base64Data = title[7..];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stringData = Encoding.UTF8.GetString(Convert.FromBase64String(base64Data));
|
||||||
|
|
||||||
|
return stringData[..(Math.Min(stringData.Length, 100))];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "New subscription";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return title[..(Math.Min(title.Length, 100))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetServerTitle(string title, out string region)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(title))
|
||||||
|
{
|
||||||
|
region = string.Empty;
|
||||||
|
return "New server";
|
||||||
|
}
|
||||||
|
|
||||||
|
title = title[1..]; // skip the leading '#' character
|
||||||
|
|
||||||
|
region = FlagEmojiParser.ExtractCountryCodes(title, out var countryIndicators).FirstOrDefault() ?? string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(region))
|
||||||
|
{
|
||||||
|
title = title.Replace(countryIndicators.FirstOrDefault() ?? string.Empty, string.Empty).Trim();
|
||||||
|
return title[..Math.Min(title.Length, 100)];
|
||||||
|
}
|
||||||
|
return title[..Math.Min(title.Length, 100)];
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region SubscriptionParsing
|
||||||
|
private static (long Upload, long Download, long Total, DateTime Expire) ParseNetworkData(string data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(data))
|
||||||
|
throw new ArgumentException("Input string cannot be null or empty.", nameof(data));
|
||||||
|
|
||||||
|
// Dictionary to temporarily store the parsed key-value pairs
|
||||||
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Split the string by semicolon to get individual pairs (e.g., "upload=0")
|
||||||
|
var pairs = data.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var keyValue in from string pair in pairs
|
||||||
|
let keyValue = pair.Split('=', 2)
|
||||||
|
where keyValue.Length == 2
|
||||||
|
select keyValue)
|
||||||
|
{
|
||||||
|
values[keyValue[0].Trim()] = keyValue[1].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to safely parse numbers, defaulting to 0 if missing or invalid
|
||||||
|
long GetLongValue(string key) =>
|
||||||
|
values.TryGetValue(key, out var val) && long.TryParse(val, out var result) ? result : 0;
|
||||||
|
|
||||||
|
var upload = GetLongValue("upload");
|
||||||
|
var download = GetLongValue("download");
|
||||||
|
var total = GetLongValue("total");
|
||||||
|
var expireUnix = GetLongValue("expire");
|
||||||
|
|
||||||
|
var expireDateTime = DateTimeOffset.FromUnixTimeSeconds(expireUnix).UtcDateTime;
|
||||||
|
|
||||||
|
return (upload, download, total, expireDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^[A-Za-z0-9+/]*={0,2}$")]
|
||||||
|
private static partial Regex Base64Regex();
|
||||||
|
|
||||||
|
private static List<SubscriptionServer> ParseServerData(string data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(data)) return [];
|
||||||
|
|
||||||
|
data = data.Trim();
|
||||||
|
|
||||||
|
if (TryGetBase64String(data, out var decodedText))
|
||||||
|
{
|
||||||
|
return [.. ParseText(decodedText)];
|
||||||
|
}
|
||||||
|
else if (data.StartsWith('{') || data.StartsWith('['))
|
||||||
|
{
|
||||||
|
return ParseJson(data);
|
||||||
|
}
|
||||||
|
else if (data.StartsWith("---") || data.Contains(':'))
|
||||||
|
{
|
||||||
|
return ParseYaml(data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return [.. ParseText(data)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ServersParsing
|
||||||
|
private static List<SubscriptionServer> ParseJson(string jsonData)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SubscriptionServer> ParseYaml(string yamlData)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<SubscriptionServer> ParseText(string textData)
|
||||||
|
{
|
||||||
|
foreach (var line in textData.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var server = ParseServerFromLink(line);
|
||||||
|
if (server != null)
|
||||||
|
{
|
||||||
|
yield return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetBase64String(string input, out string decodedText)
|
||||||
|
{
|
||||||
|
decodedText = null;
|
||||||
|
|
||||||
|
// Sanitize edge-case variants: URL-safe Base64 fixes ('-' -> '+', '_' -> '/')
|
||||||
|
var sanitized = input.Replace('-', '+').Replace('_', '/').Replace("\r", "").Replace("\n", "").Trim();
|
||||||
|
|
||||||
|
// Fix missing padding if client/server skipped it
|
||||||
|
var mod = sanitized.Length % 4;
|
||||||
|
if (mod > 0) sanitized += new string('=', 4 - mod);
|
||||||
|
|
||||||
|
// Regex validation for pure Base64 characters
|
||||||
|
if (!Base64Regex().IsMatch(sanitized)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(sanitized);
|
||||||
|
decodedText = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubscriptionServer ParseServerFromLink(string serverData)
|
||||||
|
{
|
||||||
|
var uri = new Uri(serverData);
|
||||||
|
|
||||||
|
if (uri.Scheme == "ss" || uri.Scheme == "vmess" || uri.Scheme == "vless" || uri.Scheme == "trojan")
|
||||||
|
{
|
||||||
|
var title = Uri.UnescapeDataString(uri.Fragment);
|
||||||
|
return new SubscriptionServer
|
||||||
|
{
|
||||||
|
DefaultTitle = GetServerTitle(title, out var region),
|
||||||
|
Region = FlagEmojiParser.ExtractCountryCodes(title).FirstOrDefault() ?? string.Empty,
|
||||||
|
ConnectionString = serverData,
|
||||||
|
Tags = GetTags(serverData)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tags
|
||||||
|
private static List<string> GetTags(string connection)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(connection)) return null;
|
||||||
|
|
||||||
|
if (connection.StartsWith('{') || connection.StartsWith('[')) return [.. GetJsonTags(connection)];
|
||||||
|
else return [.. GetTextTags(connection)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetTextTags(string connection)
|
||||||
|
{
|
||||||
|
var input = connection.Trim();
|
||||||
|
var scheme = string.Empty;
|
||||||
|
var query = string.Empty;
|
||||||
|
var lowered = input.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (Uri.TryCreate(input, UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
scheme = uri.Scheme ?? string.Empty;
|
||||||
|
query = uri.Query ?? string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var m = UrlCheck().Match(input);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
scheme = m.Groups[1].Value;
|
||||||
|
var idx = input.IndexOf('?');
|
||||||
|
if (idx >= 0) query = input[idx..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Protocol tag (Uri.Scheme)
|
||||||
|
if (!string.IsNullOrEmpty(scheme))
|
||||||
|
yield return scheme.ToUpperInvariant();
|
||||||
|
|
||||||
|
// 2) Security tag (one of: reality, vision, tls)
|
||||||
|
var security = string.Empty;
|
||||||
|
if (lowered.Contains("reality") || scheme.Contains("reality", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
security = "reality";
|
||||||
|
}
|
||||||
|
else if (lowered.Contains("vision") || scheme.Contains("vision", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
security = "vision";
|
||||||
|
}
|
||||||
|
else if (scheme.EndsWith("+tls", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
lowered.Contains("tls") ||
|
||||||
|
lowered.Contains("secure=true") ||
|
||||||
|
SecurityCheck().IsMatch(query))
|
||||||
|
{
|
||||||
|
security = "tls";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(security))
|
||||||
|
yield return security.ToUpperInvariant();
|
||||||
|
|
||||||
|
// 3) Transport tag (one of: tcp, ws, grpc, h2, xhttp)
|
||||||
|
var transport = string.Empty;
|
||||||
|
|
||||||
|
if (scheme.Equals("ws", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
lowered.Contains("transport=ws") ||
|
||||||
|
lowered.Contains("websocket"))
|
||||||
|
{
|
||||||
|
transport = "ws";
|
||||||
|
}
|
||||||
|
else if (scheme.Contains("grpc", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
lowered.Contains("transport=grpc") ||
|
||||||
|
lowered.Contains("grpc/"))
|
||||||
|
{
|
||||||
|
transport = "grpc";
|
||||||
|
}
|
||||||
|
else if (scheme.Equals("h2", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
lowered.Contains("http2") ||
|
||||||
|
lowered.Contains("alpn=h2") ||
|
||||||
|
lowered.Contains("transport=h2"))
|
||||||
|
{
|
||||||
|
transport = "h2";
|
||||||
|
}
|
||||||
|
else if (lowered.Contains("xhttp") || lowered.Contains("x-http") || lowered.Contains("transport=xhttp"))
|
||||||
|
{
|
||||||
|
transport = "xhttp";
|
||||||
|
}
|
||||||
|
else if (scheme.Equals("tcp", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
lowered.Contains("transport=tcp") ||
|
||||||
|
lowered.Contains("tcp/") ||
|
||||||
|
lowered.Contains(':'))
|
||||||
|
{
|
||||||
|
transport = "tcp";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transport = "tcp";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(transport))
|
||||||
|
yield return transport.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetJsonTags(string json)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^([a-zA-Z][\w+\-\.]*):")]
|
||||||
|
private static partial Regex UrlCheck();
|
||||||
|
[GeneratedRegex(@"(\b|&)security=(tls|true)(&|$)", RegexOptions.IgnoreCase, "en-001")]
|
||||||
|
private static partial Regex SecurityCheck();
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ namespace WinWARP.Core.Services.Interfaces;
|
|||||||
public interface IDialogService
|
public interface IDialogService
|
||||||
{
|
{
|
||||||
Task<(CommonDialogResult Result, string Title, string URL, int UpdateInterval)> ShowAddSubscriptionAsync();
|
Task<(CommonDialogResult Result, string Title, string URL, int UpdateInterval)> ShowAddSubscriptionAsync();
|
||||||
|
Task ShowErrorAsync(string message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ public partial class HomeViewModel(INavigationService navigationService) : Obser
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void TestTransition()
|
private void TestTransition()
|
||||||
{
|
{
|
||||||
_navigationService.NavigateTo<SettingsViewModel>();
|
_navigationService.NavigateTo<SubsSettingsViewModel>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@@ -15,8 +16,11 @@ public abstract partial class ServerItemViewModel : ObservableObject
|
|||||||
Id = server.Id;
|
Id = server.Id;
|
||||||
DefaultTitle = server.DefaultTitle;
|
DefaultTitle = server.DefaultTitle;
|
||||||
Title = server.Title;
|
Title = server.Title;
|
||||||
ConnectionString = server.ConnectionString;
|
|
||||||
Region = server.Region;
|
Region = server.Region;
|
||||||
|
ConnectionString = server.ConnectionString;
|
||||||
|
IsDeleted = server.IsDeleted;
|
||||||
|
IsDisabled = server.IsDisabled;
|
||||||
|
Tags = new ObservableCollection<string>(server.Tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Id
|
public int Id
|
||||||
@@ -68,4 +72,16 @@ public abstract partial class ServerItemViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<string> Tags { get; } = [];
|
public ObservableCollection<string> Tags { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsDeleted
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsDisabled
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public partial class SubsSettingsViewModel(ISubscriptionDbService dbService, ISu
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<SubscriptionViewModel> Subscriptions { get; } = [];
|
public ObservableCollection<SubscriptionViewModel> Subscriptions { get; } = [];
|
||||||
public ObservableCollection<ServerItemViewModel> Servers { get; } = [];
|
public ObservableCollection<StandaloneServerViewModel> Servers { get; } = [];
|
||||||
|
|
||||||
public async Task LoadSubscriptionsAsync()
|
public async Task LoadSubscriptionsAsync()
|
||||||
{
|
{
|
||||||
@@ -37,8 +37,6 @@ public partial class SubsSettingsViewModel(ISubscriptionDbService dbService, ISu
|
|||||||
Subscriptions.Add(new SubscriptionViewModel(subscription));
|
Subscriptions.Add(new SubscriptionViewModel(subscription));
|
||||||
}
|
}
|
||||||
|
|
||||||
Servers.Add(new StandaloneServerViewModel(new StandaloneServer() { Region = "NO", Title = "Not default title", DefaultTitle = "Default title" }) { Tags = { "VLESS", "GRPC", "REALITY" } });
|
|
||||||
|
|
||||||
ShowLoadingIndicator = false;
|
ShowLoadingIndicator = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +64,10 @@ public partial class SubsSettingsViewModel(ISubscriptionDbService dbService, ISu
|
|||||||
subscription.Title = customName;
|
subscription.Title = customName;
|
||||||
subscription.UpdateInterval = updateInterval;
|
subscription.UpdateInterval = updateInterval;
|
||||||
|
|
||||||
Subscriptions.Add(new SubscriptionViewModel(subscription));
|
var sub = new SubscriptionViewModel(subscription);
|
||||||
foreach (var server in subscription.Servers)
|
|
||||||
{
|
Subscriptions.Add(sub);
|
||||||
Servers.Add(new SubscriptionServerViewModel(server) { Tags = { "VLESS", "WS", "TLS" } });
|
|
||||||
}
|
|
||||||
ShowLoadingIndicator = false;
|
ShowLoadingIndicator = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@@ -21,6 +22,8 @@ public partial class SubscriptionViewModel : ObservableValidator
|
|||||||
Title = subscription.Title;
|
Title = subscription.Title;
|
||||||
UpdateInterval = subscription.UpdateInterval;
|
UpdateInterval = subscription.UpdateInterval;
|
||||||
SupportURL = subscription.SupportURL;
|
SupportURL = subscription.SupportURL;
|
||||||
|
|
||||||
|
Servers = new ObservableCollection<SubscriptionServerViewModel>(subscription.Servers.Select(s => new SubscriptionServerViewModel(s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -30,12 +33,16 @@ public partial class SubscriptionViewModel : ObservableValidator
|
|||||||
public partial string WebPage { get; set; }
|
public partial string WebPage { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(GetTitle))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsNotDefaultTitle))]
|
||||||
public partial string DefaultTitle { get; set; }
|
public partial string DefaultTitle { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial int DefaultUpdateInterval { get; set; }
|
public partial int DefaultUpdateInterval { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(GetTitle))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsNotDefaultTitle))]
|
||||||
public partial string Title { get; set; }
|
public partial string Title { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -63,4 +70,70 @@ public partial class SubscriptionViewModel : ObservableValidator
|
|||||||
{
|
{
|
||||||
return IsDirty && !HasErrors;
|
return IsDirty && !HasErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<SubscriptionServerViewModel> Servers
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task DeleteAsync()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EditNameAsync()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsNameEditing
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = false;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public void EditName()
|
||||||
|
{
|
||||||
|
if (IsNameEditing)
|
||||||
|
EditNameAsync();
|
||||||
|
IsNameEditing = !IsNameEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsDeleted
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsDisabled
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetUidForNameEditing(bool isNameEditing)
|
||||||
|
{
|
||||||
|
if (isNameEditing) return "ServerListElementMenuFlyoutSave";
|
||||||
|
else return "ServerListElementMenuFlyoutEdit";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetGlyphForNameEditing(bool isNameEditing)
|
||||||
|
{
|
||||||
|
if (isNameEditing) return "\uE792";
|
||||||
|
else return "\uE70F";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetTitle
|
||||||
|
{
|
||||||
|
get => string.IsNullOrEmpty(Title) ? (string.IsNullOrEmpty(DefaultTitle) ? string.Empty : DefaultTitle) : Title;
|
||||||
|
set => throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsNotDefaultTitle
|
||||||
|
{
|
||||||
|
get => (!string.IsNullOrEmpty(Title) && Title != DefaultTitle);
|
||||||
|
set => throw new InvalidOperationException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ public partial class BoolToVisibilityConverter : MarkupExtension, IValueConverte
|
|||||||
flag = boolValue;
|
flag = boolValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsInverted)
|
var parameterInverted = false;
|
||||||
|
if (parameter is string paramStr && bool.TryParse(paramStr, out var result))
|
||||||
|
{
|
||||||
|
parameterInverted = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInverted || parameterInverted)
|
||||||
{
|
{
|
||||||
flag = !flag;
|
flag = !flag;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,10 @@ using WinWARP.Ui.Views.Dialogs;
|
|||||||
|
|
||||||
namespace WinWARP.Ui.Services;
|
namespace WinWARP.Ui.Services;
|
||||||
|
|
||||||
public class DialogService : IDialogService
|
public class DialogService(XamlRootTracker tracker, ILocalizationService localizationService) : IDialogService
|
||||||
{
|
{
|
||||||
private readonly XamlRootTracker _tracker;
|
private readonly XamlRootTracker _tracker = tracker;
|
||||||
|
private readonly ILocalizationService _localizationService = localizationService;
|
||||||
public DialogService(XamlRootTracker tracker)
|
|
||||||
{
|
|
||||||
_tracker = tracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(CommonDialogResult Result, string Title, string URL, int UpdateInterval)> ShowAddSubscriptionAsync()
|
public async Task<(CommonDialogResult Result, string Title, string URL, int UpdateInterval)> ShowAddSubscriptionAsync()
|
||||||
{
|
{
|
||||||
@@ -44,4 +40,17 @@ public class DialogService : IDialogService
|
|||||||
|
|
||||||
return (mappedResult, dialog.ViewModel.CustomName, dialog.ViewModel.SubscriptionLink, dialog.ViewModel.UpdateInterval);
|
return (mappedResult, dialog.ViewModel.CustomName, dialog.ViewModel.SubscriptionLink, dialog.ViewModel.UpdateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowErrorAsync(string message)
|
||||||
|
{
|
||||||
|
var dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
Title = _localizationService.GetString("ErrorDialog.Title"),
|
||||||
|
Content = message,
|
||||||
|
CloseButtonText = _localizationService.GetString("ErrorDialog.CloseButtonText"),
|
||||||
|
XamlRoot = _tracker.XamlRoot,
|
||||||
|
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style
|
||||||
|
};
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,9 +213,6 @@
|
|||||||
<data name="SettingsDNSSettingsPageTitle.Text" xml:space="preserve">
|
<data name="SettingsDNSSettingsPageTitle.Text" xml:space="preserve">
|
||||||
<value>DNS Settings</value>
|
<value>DNS Settings</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SubsSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
|
||||||
<value>Subscriptions and servers</value>
|
|
||||||
</data>
|
|
||||||
<data name="ConfigSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
<data name="ConfigSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
||||||
<value>Configs settings</value>
|
<value>Configs settings</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -267,4 +264,73 @@
|
|||||||
<data name="UpdateInterval_auto" xml:space="preserve">
|
<data name="UpdateInterval_auto" xml:space="preserve">
|
||||||
<value>Auto</value>
|
<value>Auto</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ErrorDialog.CloseButtonText" xml:space="preserve">
|
||||||
|
<value>OK</value>
|
||||||
|
</data>
|
||||||
|
<data name="ErrorDialog.Title" xml:space="preserve">
|
||||||
|
<value>Error occurred:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutEdit.Text" xml:space="preserve">
|
||||||
|
<value>Edit name</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutIsDisabled.Text" xml:space="preserve">
|
||||||
|
<value>Disable server</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutDelete.Text" xml:space="preserve">
|
||||||
|
<value>Delete server</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementTitleTextBox.PlaceholderText" xml:space="preserve">
|
||||||
|
<value>New title...</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutSave.Text" xml:space="preserve">
|
||||||
|
<value>Save name</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddSub.Label" xml:space="preserve">
|
||||||
|
<value>Add subscription</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelSub.Label" xml:space="preserve">
|
||||||
|
<value>Select subscriptions</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelServ.Label" xml:space="preserve">
|
||||||
|
<value>Select servers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelAll.Label" xml:space="preserve">
|
||||||
|
<value>Select all</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandEnable.Label" xml:space="preserve">
|
||||||
|
<value>Enable selected</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandDisable.Label" xml:space="preserve">
|
||||||
|
<value>Disable selected</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandDelete.Label" xml:space="preserve">
|
||||||
|
<value>Delete selected</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
||||||
|
<value>Subscriptions and servers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddServLink.Label" xml:space="preserve">
|
||||||
|
<value>Add server</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandTabSubs.Header" xml:space="preserve">
|
||||||
|
<value>Subscriptons</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandTabServ.Header" xml:space="preserve">
|
||||||
|
<value>Server</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsEmtpyText.Text" xml:space="preserve">
|
||||||
|
<value>Nothing here...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddServFile.Label" xml:space="preserve">
|
||||||
|
<value>Import config</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementTitleTextBox.PlaceholderText" xml:space="preserve">
|
||||||
|
<value>New title...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementMenuFlyoutDelete.Text" xml:space="preserve">
|
||||||
|
<value>Delete subscription</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementMenuFlyoutIsDisabled.Text" xml:space="preserve">
|
||||||
|
<value>Disable subscription</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -213,9 +213,6 @@
|
|||||||
<data name="SettingsDNSSettingsPageTitle.Text" xml:space="preserve">
|
<data name="SettingsDNSSettingsPageTitle.Text" xml:space="preserve">
|
||||||
<value>Настройки DNS</value>
|
<value>Настройки DNS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SubsSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
|
||||||
<value>Подписки и сервера</value>
|
|
||||||
</data>
|
|
||||||
<data name="ConfigSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
<data name="ConfigSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
||||||
<value>Конфигурации</value>
|
<value>Конфигурации</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -267,4 +264,73 @@
|
|||||||
<data name="UpdateInterval_auto" xml:space="preserve">
|
<data name="UpdateInterval_auto" xml:space="preserve">
|
||||||
<value>Автоматически</value>
|
<value>Автоматически</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ErrorDialog.CloseButtonText" xml:space="preserve">
|
||||||
|
<value>Ок</value>
|
||||||
|
</data>
|
||||||
|
<data name="ErrorDialog.Title" xml:space="preserve">
|
||||||
|
<value>Произошла ошибка:</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutEdit.Text" xml:space="preserve">
|
||||||
|
<value>Изменить имя</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutIsDisabled.Text" xml:space="preserve">
|
||||||
|
<value>Выключить сервер</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutDelete.Text" xml:space="preserve">
|
||||||
|
<value>Удалить сервер</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementTitleTextBox.PlaceholderText" xml:space="preserve">
|
||||||
|
<value>Новое название</value>
|
||||||
|
</data>
|
||||||
|
<data name="ServerListElementMenuFlyoutSave.Text" xml:space="preserve">
|
||||||
|
<value>Сохранить имя</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddSub.Label" xml:space="preserve">
|
||||||
|
<value>Добавить подписку</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelSub.Label" xml:space="preserve">
|
||||||
|
<value>Выбрать подписки</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelServ.Label" xml:space="preserve">
|
||||||
|
<value>Выбрать сервера</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandSelAll.Label" xml:space="preserve">
|
||||||
|
<value>Выбрать всё</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandEnable.Label" xml:space="preserve">
|
||||||
|
<value>Включить выбранные</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandDisable.Label" xml:space="preserve">
|
||||||
|
<value>Отключить выбранные</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandCommandDelete.Label" xml:space="preserve">
|
||||||
|
<value>Удалить выбранные</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsPageNavPaneTitle.Content" xml:space="preserve">
|
||||||
|
<value>Подписки и сервера</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddServLink.Label" xml:space="preserve">
|
||||||
|
<value>Добавить сервер</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandTabSubs.Header" xml:space="preserve">
|
||||||
|
<value>Подписки</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandTabServ.Header" xml:space="preserve">
|
||||||
|
<value>Сервера</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsEmtpyText.Text" xml:space="preserve">
|
||||||
|
<value>Здесь пусто...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubsSettingsCommandAddServFile.Label" xml:space="preserve">
|
||||||
|
<value>Импортировать файл</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementTitleTextBox.PlaceholderText" xml:space="preserve">
|
||||||
|
<value>Новое имя...</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementMenuFlyoutDelete.Text" xml:space="preserve">
|
||||||
|
<value>Удалить подписку</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionListElementMenuFlyoutIsDisabled.Text" xml:space="preserve">
|
||||||
|
<value>Выключить подписку</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
xmlns:w="using:WinWARP.Core.ViewModels.Wrappers"
|
xmlns:w="using:WinWARP.Core.ViewModels.Wrappers"
|
||||||
xmlns:conv="using:WinWARP.Ui.Converters"
|
xmlns:conv="using:WinWARP.Ui.Converters"
|
||||||
xmlns:h="using:WinWARP.Ui.Helpers"
|
xmlns:h="using:WinWARP.Ui.Helpers"
|
||||||
|
xmlns:u="using:WinUI3Localizer"
|
||||||
Height="64" Width="300"
|
Height="64" Width="300"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
@@ -23,15 +24,30 @@
|
|||||||
<StackPanel>
|
<StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<conv:BoolToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<UserControl.ContextFlyout>
|
<UserControl.ContextFlyout>
|
||||||
<MenuFlyout x:Name="ServerListFlyout" >
|
<MenuFlyout x:Name="ServerListFlyout" >
|
||||||
<MenuFlyoutItem Text="Option 1"/>
|
<MenuFlyoutItem Command="{x:Bind ServerData.EditNameCommand, Mode=OneWay}" u:Uids.Uid="{x:Bind GetUidForNameEditing(ServerData.IsNameEditing), Mode=OneWay}">
|
||||||
<MenuFlyoutItem Text="Option 2"/>
|
<MenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="{x:Bind GetGlyphForNameEditing(ServerData.IsNameEditing), Mode=OneWay}" />
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<ToggleMenuFlyoutItem u:Uids.Uid="ServerListElementMenuFlyoutIsDisabled" IsChecked="{x:Bind ServerData.IsDisabled, Mode=TwoWay}">
|
||||||
|
<ToggleMenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</ToggleMenuFlyoutItem.Icon>
|
||||||
|
</ToggleMenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator/>
|
||||||
|
<MenuFlyoutItem u:Uids.Uid="ServerListElementMenuFlyoutDelete" Command="{x:Bind ServerData.DeleteCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
</MenuFlyoutItem>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</UserControl.ContextFlyout>
|
</UserControl.ContextFlyout>
|
||||||
|
|
||||||
<Grid MaxHeight="64">
|
<Grid MaxHeight="64" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="4">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="64"/>
|
<ColumnDefinition Width="64"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
@@ -55,10 +71,11 @@
|
|||||||
<Rectangle Grid.Column="1" Grid.Row="1" Fill="Beige"/>
|
<Rectangle Grid.Column="1" Grid.Row="1" Fill="Beige"/>
|
||||||
<Rectangle Grid.Column="1" Grid.Row="2" Fill="Brown"/>-->
|
<Rectangle Grid.Column="1" Grid.Row="2" Fill="Brown"/>-->
|
||||||
<StackPanel Grid.Column="1">
|
<StackPanel Grid.Column="1">
|
||||||
<TextBlock Text="{x:Bind Title(ServerData), Mode=OneWay}" Style="{StaticResource BodyTextBlockStyle}"/>
|
<TextBox Text="{x:Bind ServerData.Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" u:Uids.Uid="ServerListElementTitleTextBox" Style="{StaticResource BodyTextBlockStyle}" Visibility="{x:Bind ServerData.IsNameEditing, Converter={StaticResource BoolToVisConverter}, Mode=OneWay}"/>
|
||||||
|
<TextBlock Text="{x:Bind GetTitle(ServerData), Mode=OneWay}" Style="{StaticResource BodyTextBlockStyle}" Visibility="{x:Bind ServerData.IsNameEditing, Converter={StaticResource BoolToVisConverter}, ConverterParameter=True, Mode=OneWay}"/>
|
||||||
<TextBlock Text="{x:Bind ServerData.DefaultTitle, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" Visibility="{x:Bind IsNotDefaultTitle(ServerData), Mode=OneWay}"/>
|
<TextBlock Text="{x:Bind ServerData.DefaultTitle, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" Visibility="{x:Bind IsNotDefaultTitle(ServerData), Mode=OneWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="2" Height="24" Width="24" Padding="0" Margin="4" VerticalAlignment="Top" Click="MenuButton_Click">
|
<Button Grid.RowSpan="2" Grid.Column="2" Height="20" Width="20" Padding="0" Margin="4" VerticalAlignment="Bottom" Click="MenuButton_Click">
|
||||||
<FontIcon Glyph="" FontSize="10"/>
|
<FontIcon Glyph="" FontSize="10"/>
|
||||||
</Button>
|
</Button>
|
||||||
<ItemsView Grid.Column="1" Grid.Row="1" ItemsSource="{x:Bind ServerData.Tags, Mode=OneWay}" VerticalAlignment="Center" SelectionMode="None">
|
<ItemsView Grid.Column="1" Grid.Row="1" ItemsSource="{x:Bind ServerData.Tags, Mode=OneWay}" VerticalAlignment="Center" SelectionMode="None">
|
||||||
@@ -76,7 +93,7 @@
|
|||||||
</ItemsView.ItemTemplate>
|
</ItemsView.ItemTemplate>
|
||||||
</ItemsView>
|
</ItemsView>
|
||||||
<!--<TextBlock Grid.Column="1" Text="both server"/>-->
|
<!--<TextBlock Grid.Column="1" Text="both server"/>-->
|
||||||
<!--<ContentPresenter Grid.Column="1" Grid.Row="1" Content="{x:Bind ServerData}" ContentTemplate="{x:Bind ChooseTemplate(ServerData), Mode=OneWay}"/>-->
|
<ContentPresenter Grid.Column="1" Grid.Row="1" Content="{x:Bind ServerData}" ContentTemplate="{x:Bind ChooseTemplate(ServerData), Mode=OneWay}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed partial class ServerListElement : UserControl
|
|||||||
|
|
||||||
public static readonly DependencyProperty ServerDataProperty =
|
public static readonly DependencyProperty ServerDataProperty =
|
||||||
DependencyProperty.Register(nameof(ServerData), typeof(ServerItemViewModel),
|
DependencyProperty.Register(nameof(ServerData), typeof(ServerItemViewModel),
|
||||||
typeof(ServerListElement), new PropertyMetadata(null));
|
typeof(ServerListElement), new PropertyMetadata(null, OnServerDataChanged));
|
||||||
|
|
||||||
public ServerItemViewModel ServerData
|
public ServerItemViewModel ServerData
|
||||||
{
|
{
|
||||||
@@ -35,7 +35,7 @@ public sealed partial class ServerListElement : UserControl
|
|||||||
set => SetValue(ServerDataProperty, value);
|
set => SetValue(ServerDataProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Title(ServerItemViewModel serverData) => string.IsNullOrEmpty(serverData?.Title) ? (string.IsNullOrEmpty(serverData?.DefaultTitle) ? string.Empty : serverData.DefaultTitle) : serverData.Title;
|
public string GetTitle(ServerItemViewModel serverData) => string.IsNullOrEmpty(serverData?.Title) ? (string.IsNullOrEmpty(serverData?.DefaultTitle) ? string.Empty : serverData.DefaultTitle) : serverData.Title;
|
||||||
|
|
||||||
public Visibility IsNotDefaultTitle(ServerItemViewModel serverData) => (!string.IsNullOrEmpty(serverData?.Title) && serverData?.Title != serverData?.DefaultTitle) ? Visibility.Visible : Visibility.Collapsed;
|
public Visibility IsNotDefaultTitle(ServerItemViewModel serverData) => (!string.IsNullOrEmpty(serverData?.Title) && serverData?.Title != serverData?.DefaultTitle) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
@@ -80,4 +80,28 @@ public sealed partial class ServerListElement : UserControl
|
|||||||
ServerListFlyout.ShowAt(button, options);
|
ServerListFlyout.ShowAt(button, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnServerDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var control = (ServerListElement)d;
|
||||||
|
if (e.OldValue is ServerItemViewModel oldVM) oldVM.PropertyChanged -= control.OnServerItemPropertyChanged;
|
||||||
|
if (e.NewValue is ServerItemViewModel newVM) newVM.PropertyChanged += control.OnServerItemPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServerItemPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ServerItemViewModel.Title)) Bindings.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetUidForNameEditing(bool isNameEditing)
|
||||||
|
{
|
||||||
|
if (isNameEditing) return "ServerListElementMenuFlyoutSave";
|
||||||
|
else return "ServerListElementMenuFlyoutEdit";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetGlyphForNameEditing(bool isNameEditing)
|
||||||
|
{
|
||||||
|
if (isNameEditing) return "\uE792";
|
||||||
|
else return "\uE70F";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,147 @@
|
|||||||
xmlns:local="using:WinWARP.Ui.Views"
|
xmlns:local="using:WinWARP.Ui.Views"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:com="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:conv="using:WinWARP.Ui.Converters"
|
||||||
xmlns:c="using:WinWARP.Ui.Views.Components"
|
xmlns:c="using:WinWARP.Ui.Views.Components"
|
||||||
xmlns:vm="using:WinWARP.Core.ViewModels"
|
xmlns:vm="using:WinWARP.Core.ViewModels"
|
||||||
|
xmlns:w="using:WinWARP.Core.ViewModels.Wrappers"
|
||||||
|
xmlns:u="using:WinUI3Localizer"
|
||||||
|
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<StackPanel>
|
<Page.Resources>
|
||||||
<Button Command="{x:Bind ViewModel.AddSubscriptionCommand}" Content="Add Subscription"/>
|
<conv:BoolToVisibilityConverter x:Key="BoolToVisConverter" />
|
||||||
<GridView ItemsSource="{x:Bind ViewModel.Servers}" SelectionMode="None">
|
</Page.Resources>
|
||||||
<GridView.ItemContainerStyle>
|
<Grid>
|
||||||
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
<Grid.RowDefinitions>
|
||||||
<Setter Property="Margin" Value="6"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Style>
|
<RowDefinition Height="*"/>
|
||||||
</GridView.ItemContainerStyle>
|
</Grid.RowDefinitions>
|
||||||
|
<com:TabbedCommandBar Margin="8,0">
|
||||||
|
<com:TabbedCommandBar.PaneFooter>
|
||||||
|
<CommandBar Background="Transparent" DefaultLabelPosition="Right">
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandCommandEnable">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandCommandDisable">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandCommandDelete">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</CommandBar>
|
||||||
|
</com:TabbedCommandBar.PaneFooter>
|
||||||
|
<com:TabbedCommandBar.MenuItems>
|
||||||
|
<com:TabbedCommandBarItem u:Uids.Uid="SubsSettingsCommandTabSubs">
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandAddSub" Command="{x:Bind ViewModel.AddSubscriptionCommand}">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
<AppBarSeparator/>
|
||||||
|
<AppBarToggleButton u:Uids.Uid="SubsSettingsCommandSelSub">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandSelAll">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</com:TabbedCommandBarItem>
|
||||||
|
<com:TabbedCommandBarItem u:Uids.Uid="SubsSettingsCommandTabServ">
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandAddServLink">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandAddServFile">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
<AppBarSeparator/>
|
||||||
|
<AppBarToggleButton u:Uids.Uid="SubsSettingsCommandSelServ">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
<AppBarButton u:Uids.Uid="SubsSettingsCommandSelAll">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</com:TabbedCommandBarItem>
|
||||||
|
</com:TabbedCommandBar.MenuItems>
|
||||||
|
</com:TabbedCommandBar>
|
||||||
|
|
||||||
<GridView.ItemTemplate>
|
<ScrollView Grid.Row="1" CornerRadius="4">
|
||||||
<DataTemplate x:DataType="vm:ServerItemViewModel">
|
<ItemsView ItemsSource="{x:Bind ViewModel.Subscriptions}" SelectionMode="Multiple" HorizontalAlignment="Stretch">
|
||||||
<c:ServerListElement ServerData="{x:Bind}"/>
|
<ItemsView.Layout>
|
||||||
</DataTemplate>
|
<StackLayout/>
|
||||||
</GridView.ItemTemplate>
|
</ItemsView.Layout>
|
||||||
</GridView>
|
<ItemsView.ItemTemplate>
|
||||||
<ProgressRing IsActive="{x:Bind ViewModel.ShowLoadingIndicator, Mode=OneWay}"/>
|
<DataTemplate x:DataType="w:SubscriptionViewModel">
|
||||||
|
<ItemContainer Margin="8,8,8,0" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" VerticalContentAlignment="Center">
|
||||||
|
<Expander HorizontalAlignment="Stretch" Margin="0,0,28,0">
|
||||||
|
<Expander.ContextFlyout>
|
||||||
|
<MenuFlyout x:Name="SubscritionListFlyout">
|
||||||
|
<MenuFlyoutItem u:Uids.Uid="{x:Bind GetUidForNameEditing(IsNameEditing), Mode=OneWay}" Command="{x:Bind EditNameCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="{x:Bind GetGlyphForNameEditing(IsNameEditing), Mode=OneWay}"/>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<ToggleMenuFlyoutItem u:Uids.Uid="SubscriptionListElementMenuFlyoutIsDisabled" IsChecked="{x:Bind IsDisabled, Mode=TwoWay}">
|
||||||
|
<ToggleMenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</ToggleMenuFlyoutItem.Icon>
|
||||||
|
</ToggleMenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator/>
|
||||||
|
<MenuFlyoutItem u:Uids.Uid="SubscriptionListElementMenuFlyoutDelete" Command="{x:Bind DeleteCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Expander.ContextFlyout>
|
||||||
|
<Expander.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox Text="{x:Bind Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" u:Uids.Uid="SubscriptionListElementTitleTextBox"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}" Visibility="{x:Bind IsNameEditing, Converter={StaticResource BoolToVisConverter}, Mode=OneWay}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{x:Bind GetTitle, Mode=OneWay}" Style="{StaticResource BodyStrongTextBlockStyle}" Visibility="{x:Bind IsNameEditing, Converter={StaticResource BoolToVisConverter}, ConverterParameter=True, Mode=OneWay}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{x:Bind DefaultTitle, Mode=OneWay}" Style="{StaticResource BodyTextBlockStyle}" Visibility="{x:Bind IsNotDefaultTitle, Converter={StaticResource BoolToVisConverter}, Mode=OneWay}"
|
||||||
|
VerticalAlignment="Center" Margin="8"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</Expander.Header>
|
||||||
|
|
||||||
|
<ItemsView ItemsSource="{x:Bind Servers}" SelectionMode="Multiple">
|
||||||
|
<ItemsView.Layout>
|
||||||
|
<UniformGridLayout Orientation="Horizontal" ItemsStretch="None" ItemsJustification="SpaceAround"/>
|
||||||
|
</ItemsView.Layout>
|
||||||
|
<ItemsView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ServerItemViewModel">
|
||||||
|
<ItemContainer Margin="6">
|
||||||
|
<c:ServerListElement ServerData="{x:Bind}"/>
|
||||||
|
</ItemContainer>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsView.ItemTemplate>
|
||||||
|
</ItemsView>
|
||||||
|
</Expander>
|
||||||
|
</ItemContainer>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsView.ItemTemplate>
|
||||||
|
</ItemsView>
|
||||||
|
</ScrollView>
|
||||||
|
<TextBlock Grid.Row="1" u:Uids.Uid="SubsSettingsEmtpyText" Style="{ThemeResource BodyLargeTextBlockStyle}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
||||||
<PackageReference Include="MessageFormat" Version="8.0.0" />
|
<PackageReference Include="MessageFormat" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2705" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2705" />
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="ViewModels\" />
|
||||||
<Folder Include="Views\ConfigSettings\" />
|
<Folder Include="Views\ConfigSettings\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user