Skip to content

Commit d915c02

Browse files
committed
feat(Avalonia): custom uploader menu
1 parent 50d13eb commit d915c02

30 files changed

+2356
-375
lines changed

SnapX.Avalonia/App.axaml.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Microsoft.Extensions.DependencyInjection;
2323
using Serilog;
2424
using SnapX.Avalonia.ViewModels;
25+
using SnapX.Avalonia.ViewModels.Settings;
2526
// using SnapX.Avalonia.ViewModels.Settings;
2627
using SnapX.Avalonia.Views;
2728
using SnapX.Avalonia.Views.Settings;
@@ -31,6 +32,7 @@
3132
using SnapX.Core.Job;
3233
using SnapX.Core.Upload;
3334
using SnapX.Core.Utils;
35+
using SnapX.Core.Utils.Extensions;
3436
using SnapX.Core.Utils.Native;
3537
using Point = SixLabors.ImageSharp.Point;
3638

@@ -311,6 +313,7 @@ private void Shutdown()
311313
public void ListenForEvents()
312314
{
313315
Core.SnapX.EventAggregator.Subscribe<NeedClipboardCopyEvent>(HandleClipboardCopyEvent);
316+
Core.SnapX.EventAggregator.Subscribe<ErrorMessageEvent>(HandleErrorMessageEvent);
314317
}
315318

316319
[RequiresDynamicCode(
@@ -490,7 +493,6 @@ private static async Task<IClipboard> GetOrCreateClipboardWindowAsync()
490493
}
491494
}
492495

493-
494496
public override void OnFrameworkInitializationCompleted()
495497
{
496498
// Crashes must be contained, AT ALL COSTS!
@@ -930,8 +932,8 @@ public static void ConfigureServices(IServiceCollection services)
930932
services.AddSingleton<CustomUploaderVM>();
931933
services.AddSingleton<ImportExportVM>();
932934
services.AddTransient<ImportExportView>();
933-
// services.AddSingleton<ScreenRecordOptionsVM>();
934-
// services.AddTransient<ScreenRecordOptionsView>();
935+
services.AddSingleton<ScreenRecordOptionsVM>();
936+
services.AddTransient<ScreenRecordOptionsView>();
935937

936938
services.AddTransient<SettingsHomePageView>();
937939
services.AddSingleton<SettingsHomePageViewVM>();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Globalization;
2+
using Avalonia.Data.Converters;
3+
using SnapX.Core.Utils;
4+
5+
namespace SnapX.Avalonia.Converters;
6+
7+
public class CustomUploaderNameConverter : IMultiValueConverter
8+
{
9+
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
10+
{
11+
string? name = values.Count > 0 ? values[0] as string : null;
12+
string? url = values.Count > 1 ? values[1] as string : null;
13+
14+
if (!string.IsNullOrWhiteSpace(name))
15+
{
16+
return name;
17+
}
18+
19+
if (!string.IsNullOrWhiteSpace(url))
20+
{
21+
return URLHelpers.GetHostName(url);
22+
}
23+
24+
return "New Uploader";
25+
}
26+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Globalization;
2+
using Avalonia.Data.Converters;
3+
using SnapX.Core;
4+
5+
namespace SnapX.Avalonia.Converters;
6+
7+
public class EnumToBooleanConverter : IMultiValueConverter
8+
{
9+
// Convert: Enum (Current Value) -> Boolean (IsChecked)
10+
// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
11+
// {
12+
// if (value is Enum current && parameter is Enum target)
13+
// {
14+
// DebugHelper.WriteLine(
15+
// "EnumToBooleanConverter Convert: " + current.ToString() + " | " + target.ToString()
16+
// );
17+
// return current.HasFlag(target);
18+
// }
19+
// else
20+
// {
21+
// DebugHelper.WriteLine(
22+
// "EnumToBooleanConverter Convert: false, value is not Enum or parameter is not Enum"
23+
// );
24+
// DebugHelper.WriteLine(
25+
// "Value Type: " + (value?.GetType().ToString() ?? "null") + $"({value})"
26+
// );
27+
// DebugHelper.WriteLine(
28+
// "Parameter Type: " + (parameter?.GetType().ToString() ?? "null") + $"({parameter})"
29+
// );
30+
// }
31+
// return false;
32+
// }
33+
34+
public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
35+
{
36+
// values[0] = SelectedUploader.DestinationType
37+
// values[1] = Current Item in ItemsControl (The specific Enum value)
38+
if (values.Count >= 2 && values[0] is Enum current && values[1] is Enum target)
39+
{
40+
return current.HasFlag(target);
41+
}
42+
return false;
43+
}
44+
45+
public object[] ConvertBack(object value, Type[] targetTypes, object? parameter, CultureInfo culture)
46+
{
47+
throw new NotSupportedException();
48+
}
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Globalization;
2+
using Avalonia.Data.Converters;
3+
4+
namespace SnapX.Avalonia.Converters;
5+
6+
public class EnumToCollectionConverter : IValueConverter
7+
{
8+
public static readonly EnumToCollectionConverter Instance = new();
9+
10+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
11+
{
12+
if (value is Type enumType && enumType.IsEnum)
13+
{
14+
return Enum.GetValues(enumType);
15+
}
16+
return null;
17+
}
18+
19+
public object? ConvertBack(
20+
object? value,
21+
Type targetType,
22+
object? parameter,
23+
CultureInfo culture
24+
)
25+
{
26+
return null;
27+
}
28+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace SnapX.Avalonia.Converters;
2+
3+
using System.Globalization;
4+
using global::Avalonia.Data.Converters;
5+
6+
public class HeaderSecurityBlurConverter : IValueConverter
7+
{
8+
private static readonly string[] SensitiveKeys =
9+
{
10+
"password",
11+
"upload_password",
12+
"upload-password",
13+
"passwd",
14+
"pass",
15+
"pwd",
16+
"api",
17+
"apikey",
18+
"api-key",
19+
"api_key",
20+
"x-api-key",
21+
"api key",
22+
"key",
23+
"keyid",
24+
"key-id",
25+
"email",
26+
"user",
27+
"k", // puush.me uses 'k' as their API key header
28+
"p", // short for password
29+
"username",
30+
"user-name",
31+
"user name",
32+
"credential",
33+
"creds",
34+
"cred",
35+
"token",
36+
"secret",
37+
"auth",
38+
"authorization",
39+
"x-authorization",
40+
"x-auth-token",
41+
"access-token",
42+
"access token",
43+
"bearer",
44+
"session",
45+
"jwt",
46+
"cookie",
47+
"priv",
48+
"sid",
49+
"uuid",
50+
"guid",
51+
"salt",
52+
"nonce",
53+
};
54+
55+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
56+
{
57+
string? key = value as string;
58+
if (string.IsNullOrWhiteSpace(key))
59+
return 0.0;
60+
61+
bool isSensitive = SensitiveKeys.Any(s =>
62+
key.Equals(s, StringComparison.OrdinalIgnoreCase)
63+
);
64+
65+
return isSensitive ? 20.0 : 0.0;
66+
}
67+
68+
public object ConvertBack(
69+
object? value,
70+
Type targetType,
71+
object? parameter,
72+
CultureInfo culture
73+
) => throw new NotImplementedException();
74+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System.Collections.ObjectModel;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Net.Http.Json;
4+
using System.Text;
5+
using System.Text.Json.Serialization;
6+
using CommunityToolkit.Mvvm.ComponentModel;
7+
8+
namespace SnapX.Avalonia.ViewModels;
9+
10+
public record GithubContentItem
11+
{
12+
[JsonPropertyName("name")]
13+
public string Name { get; init; } = string.Empty;
14+
15+
[JsonPropertyName("path")]
16+
public string Path { get; init; } = string.Empty;
17+
18+
[JsonPropertyName("sha")]
19+
public string Sha { get; init; } = string.Empty;
20+
21+
[JsonPropertyName("size")]
22+
public int Size { get; init; }
23+
24+
[JsonPropertyName("url")]
25+
public string Url { get; init; } = string.Empty;
26+
27+
[JsonPropertyName("html_url")]
28+
public string HtmlUrl { get; init; } = string.Empty;
29+
30+
[JsonPropertyName("git_url")]
31+
public string GitUrl { get; init; } = string.Empty;
32+
33+
[JsonPropertyName("download_url")]
34+
public string? DownloadUrl { get; init; }
35+
36+
[JsonPropertyName("type")]
37+
public string Type { get; init; } = string.Empty; // "file" or "dir"
38+
39+
[JsonIgnore]
40+
public bool IsFile => Type == "file";
41+
42+
[JsonIgnore]
43+
public bool IsSxcu => Name.EndsWith(".sxcu", StringComparison.OrdinalIgnoreCase);
44+
}
45+
46+
[JsonSerializable(typeof(List<GithubContentItem>))]
47+
[JsonSerializable(typeof(GithubContentItem))]
48+
internal partial class GithubJsonContext : JsonSerializerContext { }
49+
50+
public partial class CustomUploaderCatalogVM : ViewModelBase
51+
{
52+
[ObservableProperty]
53+
private bool? _isAllSelected = false;
54+
55+
partial void OnIsAllSelectedChanged(bool? value)
56+
{
57+
if (value == null)
58+
return;
59+
RefreshSelectionState();
60+
foreach (var item in AvailableUploaders)
61+
item.IsSelected = value.Value;
62+
}
63+
64+
// Logic to update the CheckBox state when individual items are clicked
65+
public void RefreshSelectionState()
66+
{
67+
int selectedCount = AvailableUploaders.Count(x => x.IsSelected);
68+
69+
IsAllSelected =
70+
selectedCount == 0 ? false
71+
: selectedCount == AvailableUploaders.Count ? true
72+
: null;
73+
}
74+
75+
public static string GetFriendlyName(string fileName)
76+
{
77+
if (string.IsNullOrWhiteSpace(fileName))
78+
return string.Empty;
79+
80+
// 1. Remove extension
81+
string name = fileName.EndsWith(".sxcu", StringComparison.OrdinalIgnoreCase)
82+
? fileName[..^5]
83+
: fileName;
84+
85+
// 2. Handle CamelCase / Formatting
86+
// If the name is "myCustomUploader", this makes it "My Custom Uploader"
87+
// If the name is "imgur", it makes it "Imgur"
88+
StringBuilder sb = new StringBuilder();
89+
for (int i = 0; i < name.Length; i++)
90+
{
91+
char c = name[i];
92+
if (i == 0)
93+
{
94+
sb.Append(char.ToUpper(c));
95+
}
96+
else if (char.IsUpper(c) && !char.IsUpper(name[i - 1]))
97+
{
98+
sb.Append(' ');
99+
sb.Append(c);
100+
}
101+
else
102+
{
103+
sb.Append(c);
104+
}
105+
}
106+
107+
return sb.ToString().Replace("_", " ").Replace("-", " ").Trim();
108+
}
109+
110+
[ObservableProperty]
111+
private ObservableCollection<UploaderInfo> _availableUploaders = new();
112+
113+
[RequiresUnreferencedCode("Json type abuse")]
114+
[RequiresDynamicCode("Json type abuse")]
115+
public async Task LoadCatalogAsync()
116+
{
117+
var client = Core.Utils.Miscellaneous.HttpClientFactory.Get();
118+
119+
var response = await client.GetFromJsonAsync(
120+
"https://api.github.com/repos/SnapXL/CustomUploaders/contents/",
121+
GithubJsonContext.Default.ListGithubContentItem
122+
);
123+
124+
if (response != null)
125+
{
126+
AvailableUploaders.Clear();
127+
foreach (var file in response.Where(f => f.IsSxcu))
128+
{
129+
AvailableUploaders.Add(
130+
new UploaderInfo
131+
{
132+
Name = GetFriendlyName(file.Name),
133+
DownloadUrl = file.DownloadUrl!,
134+
}
135+
);
136+
}
137+
}
138+
}
139+
140+
public void UpdateSelection(List<UploaderInfo> selectedItems)
141+
{
142+
var selectedSet = selectedItems.ToHashSet();
143+
144+
foreach (var item in AvailableUploaders)
145+
{
146+
// This updates the property on the model inside the ObservableCollection
147+
item.IsSelected = selectedSet.Contains(item);
148+
}
149+
RefreshSelectionState();
150+
}
151+
}
152+
153+
public partial class UploaderInfo : ObservableObject
154+
{
155+
public string Name { get; init; }
156+
public string DownloadUrl { get; init; }
157+
158+
[JsonIgnore]
159+
[ObservableProperty]
160+
private bool _isSelected;
161+
}

0 commit comments

Comments
 (0)