Browse Source

updated libs, android performance fix, DI fix, ui changes

Veloe 11 tháng trước cách đây
mục cha
commit
d6fcd0d716
20 tập tin đã thay đổi với 198 bổ sung137 xóa
  1. 5 2
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Android/MainActivity.cs
  2. 4 4
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Android/VeloeAvaloniaKemonoPartyApp.Android.csproj
  3. 2 1
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Desktop/Services/DesktopStorageService.cs
  4. 4 4
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Desktop/VeloeAvaloniaKemonoPartyApp.Desktop.csproj
  5. 3 0
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/App.axaml.cs
  6. 4 4
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Creator.cs
  7. 17 34
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Post.cs
  8. 5 6
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/Extensions.cs
  9. 71 21
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/HttpClient.cs
  10. 0 9
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/RegisteredServices.cs
  11. 7 6
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.csproj
  12. 3 1
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/AttachmentViewModel.cs
  13. 4 3
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorPostsViewModel.cs
  14. 0 4
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorViewModel.cs
  15. 5 3
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorsViewModel.cs
  16. 21 21
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/PostImageViewModel.cs
  17. 2 0
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/PostViewModel.cs
  18. 9 8
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorPostsView.axaml
  19. 10 5
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorView.axaml
  20. 22 1
      VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorsView.axaml

+ 5 - 2
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Android/MainActivity.cs

@@ -3,6 +3,7 @@ using Android.Content.PM;
 using Avalonia;
 using Avalonia.Android;
 using Avalonia.ReactiveUI;
+using Splat;
 using VeloeAvaloniaKemonoPartyApp.Android.Services;
 using VeloeAvaloniaKemonoPartyApp.Services;
 
@@ -18,11 +19,13 @@ namespace VeloeAvaloniaKemonoPartyApp.Android
     {
         protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
         {
-            RegisteredServices.OpenUrlService = new AndroidOpenUrlService();
-            RegisteredServices.StorageService = new AndroidStorageService(CacheDir.AbsolutePath);
+            Locator.CurrentMutable.RegisterLazySingleton<IOpenUrlService>(() =>  new AndroidOpenUrlService());
+            Locator.CurrentMutable.RegisterLazySingleton<IStorageService>(() => new AndroidStorageService(CacheDir.AbsolutePath));
             
             return base.CustomizeAppBuilder(builder)
                 .WithInterFont()
+                .UseSkia().With(new SkiaOptions() { MaxGpuResourceSizeBytes = 512 * 1024 * 1024 })
+                .With(new AndroidPlatformOptions() { RenderingMode = new[] { AndroidRenderingMode.Vulkan } })
                 .UseReactiveUI();
         }
     }

+ 4 - 4
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Android/VeloeAvaloniaKemonoPartyApp.Android.csproj

@@ -6,11 +6,11 @@
     <Nullable>enable</Nullable>
     <ApplicationId>com.Veloe.AvaloniaKemonoParty</ApplicationId>
     <ApplicationVersion>1</ApplicationVersion>
-    <ApplicationDisplayVersion>1.0.0.160</ApplicationDisplayVersion>
+    <ApplicationDisplayVersion>1.0.0.179</ApplicationDisplayVersion>
     <AndroidPackageFormat>apk</AndroidPackageFormat>
     <AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
-    <AssemblyVersion>1.0.0.160</AssemblyVersion>
-    <FileVersion>1.0.0.160</FileVersion>
+    <AssemblyVersion>1.0.0.182</AssemblyVersion>
+    <FileVersion>1.0.0.182</FileVersion>
   </PropertyGroup>
 
   <ItemGroup>
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Avalonia.Android" Version="11.1.3" />
+    <PackageReference Include="Avalonia.Android" Version="11.2.0-rc1" />
     <PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.12" />
     <PackageReference Include="Xamarin.Essentials" Version="1.8.1" />
   </ItemGroup>

+ 2 - 1
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Desktop/Services/DesktopStorageService.cs

@@ -1,4 +1,5 @@
 using Avalonia;
+using Splat;
 using System.IO;
 using System.Reflection;
 using System.Threading.Tasks;
@@ -27,7 +28,7 @@ namespace VeloeAvaloniaKemonoPartyApp.Desktop.Services
     {
         public static AppBuilder InitStorageService(this AppBuilder appBuilder)
         {
-            RegisteredServices.StorageService = new DesktopStorageService();
+            Locator.CurrentMutable.RegisterLazySingleton<IStorageService>(()=>new DesktopStorageService());
             return appBuilder;
         }
     }

+ 4 - 4
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.Desktop/VeloeAvaloniaKemonoPartyApp.Desktop.csproj

@@ -10,14 +10,14 @@
 
   <PropertyGroup>
     <ApplicationManifest>app.manifest</ApplicationManifest>
-    <AssemblyVersion>1.0.0.135</AssemblyVersion>
-    <FileVersion>1.0.0.135</FileVersion>
+    <AssemblyVersion>1.0.0.175</AssemblyVersion>
+    <FileVersion>1.0.0.175</FileVersion>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
+    <PackageReference Include="Avalonia.Desktop" Version="11.2.0-rc1" />
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
-    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
+    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.0-rc1" />
   </ItemGroup>
 
   <ItemGroup>

+ 3 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/App.axaml.cs

@@ -6,6 +6,7 @@ using HanumanInstitute.MvvmDialogs.Avalonia;
 using HanumanInstitute.MvvmDialogs.Avalonia.MessageBox;
 using Splat;
 using VeloeAvaloniaKemonoPartyApp.ViewModels;
+using VeloeKemonoPartyApp.Services;
 
 namespace VeloeAvaloniaKemonoPartyApp
 {
@@ -21,6 +22,8 @@ namespace VeloeAvaloniaKemonoPartyApp
                     viewLocator: ApplicationLifetime is IClassicDesktopStyleApplicationLifetime ? new Dialogs.ViewLocatorDesktop() : new Dialogs.ViewLocatorSingle(),
                     dialogFactory: new DialogFactory().AddMessageBox(MessageBoxMode.Popup)),
                 viewModelFactory: x => Locator.Current.GetService(x)));
+            build.RegisterLazySingleton(() => new KemonoHttpClient());
+
 
             SplatRegistrations.Register<CreatorsViewModel>();
             SplatRegistrations.Register<CreatorPostsViewModel>();

+ 4 - 4
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Creator.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using Splat;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text.Json.Serialization;
@@ -29,13 +30,12 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
         public static async Task<IEnumerable<Creator>> SearchAsync(string searchTerm, bool reloadSource)
         {
             if (Source.Count == 0 || reloadSource)
-                Source = await s_httpClient.GetCreatorsList(reloadSource);
+                Source = await Locator.Current.GetService<KemonoHttpClient>().GetCreatorsList(reloadSource);
               
 
             return Source.Where(x=>x.Name.StartsWith(searchTerm,System.StringComparison.OrdinalIgnoreCase)).Take(50);
         }
 
-        private static KemonoHttpClient s_httpClient = new();
         private string CachePath => $"./Cache/{Service} - {Id}";
 
         public async Task<Stream> LoadCoverBitmapAsync()
@@ -48,7 +48,7 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
             {
                 try
                 {
-                    var data = await s_httpClient.httpClient.GetByteArrayAsync(Icon);
+                    var data = await Locator.Current.GetService<KemonoHttpClient>().httpClient.GetByteArrayAsync(Icon);
                     return new MemoryStream(data);
                 }
                 catch (System.Net.Http.HttpRequestException ex)

+ 17 - 34
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Post.cs

@@ -1,9 +1,9 @@
-using System;
+using Splat;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
-using System.Net.Mail;
 using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using VeloeAvaloniaKemonoPartyApp.Services;
@@ -41,31 +41,9 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
 
         public static async Task<IEnumerable<Post>> LoadPostsAsync(Creator creator, int start, string search = "")
         {
-            return await s_httpClient.GetPostsList(creator.Id, creator.Service, start, search);
+            return await Locator.Current.GetService<KemonoHttpClient>().GetPostsList(creator.Id, creator.Service, start, search);
         }
 
-        private static KemonoHttpClient s_httpClient = new();
-        private string CachePath => $"./Cache/{User} - {Published}";
-
-        public async Task<Stream> LoadCoverBitmapAsync()
-        {
-            if (System.IO.File.Exists(CachePath + ".bmp"))
-            {
-                return System.IO.File.OpenRead(CachePath + ".bmp");
-            }
-            else
-            {
-                try
-                {
-                    var data = await s_httpClient.httpClient.GetByteArrayAsync(Attachments.FirstOrDefault()?.Link);
-                    return new MemoryStream(data);
-                }
-                catch (System.Net.Http.HttpRequestException ex)
-                {
-                    return new MemoryStream();
-                }
-            }
-        }
     }
 
     public class Attachment
@@ -79,17 +57,17 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
 
         public bool IsImage => Path.EndsWith(".jpg") || Path.EndsWith(".png") || Path.EndsWith(".jpeg");
 
-        private static KemonoHttpClient s_httpClient = new();
-
-        private static StorageService s_storage = new();
 
         protected virtual string FileName => Path.Split('/').Last();
 
+        public string User { get; internal set; }
+        public string Service { get; internal set; }
+
         public async Task<Stream?> LoadCoverBitmapAsync()
         {
-            if (System.IO.File.Exists(System.IO.Path.Combine(await RegisteredServices.StorageService.GetCacheFolderAsync(),  FileName + ".bmp")))
+            if (System.IO.File.Exists(await GetPath()))
             {
-                var stream = System.IO.File.OpenRead(System.IO.Path.Combine(await RegisteredServices.StorageService.GetCacheFolderAsync(), FileName + ".bmp"));
+                var stream = System.IO.File.OpenRead(await GetPath());
 
                 byte[] hash = System.Security.Cryptography.SHA256.Create().ComputeHash(stream);
 
@@ -101,12 +79,15 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
                 }
                 stream.Close();
             }
-            
+
             try
             {
-                var data = await s_httpClient.httpClient.GetByteArrayAsync(Link);
+                var data = await Locator.Current.GetService<KemonoHttpClient>().httpClient.GetByteArrayAsync(Link);
 
-                using (var stream = System.IO.File.OpenWrite(System.IO.Path.Combine(await RegisteredServices.StorageService.GetCacheFolderAsync(), FileName + ".bmp")))
+                if (!Directory.Exists(await GetFolderPath()))
+                    Directory.CreateDirectory(await GetFolderPath());
+
+                using (var stream = System.IO.File.OpenWrite(await GetPath()))
                 {
                     await stream.WriteAsync(data);
                 }
@@ -118,9 +99,11 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
                 Debug.WriteLine(ex);
                 return null;
             }
-            
+
         }
 
+        private async Task<string> GetPath() => System.IO.Path.Combine(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync(), $"{Service}{User}/{FileName}.bmp");
+        private async Task<string> GetFolderPath() => System.IO.Path.Combine(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync(), $"{Service}{User}");
     }
 
     public class ExternalAttachment : Attachment

+ 5 - 6
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/Extensions.cs

@@ -1,12 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using Avalonia.Data.Converters;
+using System;
 
 namespace VeloeAvaloniaKemonoPartyApp.Services
 {
-    internal static class Extensions
+    public static class Extensions
     {
         public static string CreateMD5(string input)
         {
@@ -27,5 +24,7 @@ namespace VeloeAvaloniaKemonoPartyApp.Services
                 // return sb.ToString();
             }
         }
+
+        public static FuncValueConverter<double, DateTime> DoubleToTimeConverter { get; } = new FuncValueConverter<double,DateTime>( (x) => DateTimeOffset.FromUnixTimeSeconds((int)x).LocalDateTime); 
     }
 }

+ 71 - 21
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/HttpClient.cs

@@ -5,6 +5,8 @@ using System.Threading.Tasks;
 using System.Text.Json;
 using VeloeAvaloniaKemonoPartyApp.Models;
 using VeloeAvaloniaKemonoPartyApp.Services;
+using System.Linq;
+using Splat;
 
 namespace VeloeKemonoPartyApp.Services
 {
@@ -12,7 +14,10 @@ namespace VeloeKemonoPartyApp.Services
     {
         public HttpClient httpClient;
 
-        public List<Post> posts;
+        private string _postsIds = string.Empty;
+        private Queue<Post> _posts;
+        private HashSet<string> _ids;
+        private const int _postsPerLoad = 10;
         public KemonoHttpClient() 
         {
             httpClient = new HttpClient()
@@ -20,23 +25,25 @@ namespace VeloeKemonoPartyApp.Services
                 BaseAddress = new Uri("https://kemono.su"),
                 
             };
-            posts = new List<Post>();
+            _posts = new Queue<Post>(50);
+            _ids = new HashSet<string>();
         }
 
         public async Task<List<Creator>> GetCreatorsList(bool reloadSource)
         {
-            if (System.IO.File.Exists(await RegisteredServices.StorageService.GetCacheFolderAsync() + "/creators.json") && new System.IO.FileInfo(await RegisteredServices.StorageService.GetCacheFolderAsync() + "/creators.json").CreationTime > DateTime.Now.AddDays(-7) && !reloadSource)
+            if (System.IO.File.Exists(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync() + "/creators.json") && new System.IO.FileInfo(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync() + "/creators.json").CreationTime > DateTime.Now.AddDays(-7) && !reloadSource)
             {
                 try
                 {
-                    using (var stream = System.IO.File.OpenRead(await RegisteredServices.StorageService.GetCacheFolderAsync() + "/creators.json"))
+                    using (var stream = System.IO.File.OpenRead(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync() + "/creators.json"))
                     {
                         return await JsonSerializer.DeserializeAsync<List<Creator>>(stream);
                     }
                 }
                 catch 
                 {
-                    System.IO.File.Delete(await RegisteredServices.StorageService.GetCacheFolderAsync() + "/creators.json");
+                    System.IO.File.Delete(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync() + "/creators.json");
+                    throw;
                 }
             }
 
@@ -45,12 +52,13 @@ namespace VeloeKemonoPartyApp.Services
                 using (HttpResponseMessage response = await httpClient.GetAsync("https://kemono.su/api/v1/creators.txt"))
                 using (HttpContent content = response.Content)
                 {
-                    using (var stream = System.IO.File.OpenWrite(await RegisteredServices.StorageService.GetCacheFolderAsync() + "/creators.json"))
+                    var result = await JsonSerializer.DeserializeAsync<List<Creator>>(await response.Content.ReadAsStreamAsync());
+
+                    using (var stream = System.IO.File.Create(await Locator.Current.GetService<IStorageService>().GetCacheFolderAsync() + "/creators.json"))
                     {
                         stream.Write(await response.Content.ReadAsByteArrayAsync());
                     }
-
-                    return await JsonSerializer.DeserializeAsync<List<Creator>>(await response.Content.ReadAsStreamAsync());
+                    return result;
                 }
             }
             catch (Exception)
@@ -59,25 +67,67 @@ namespace VeloeKemonoPartyApp.Services
             }
         }
 
-        public async Task<List<Post>> GetPostsList(string id, string service, int start, string search = "")
+        public async Task<IEnumerable<Post>> GetPostsList(string id, string service, int start, string search = "")
         {
-            try
+            if (_posts.Count > 0 && _postsIds.Equals(service+id, StringComparison.Ordinal))
             {
-                using (HttpResponseMessage response = await httpClient.GetAsync($"https://kemono.su/api/v1/{service}/user/{id}?o={start}" + (!string.IsNullOrEmpty(search) ? $"&q={search}" : string.Empty)))
-                using (HttpContent content = response.Content)
+                var result = new List<Post>();
+                for (int i = 0; i < _postsPerLoad && _posts.Count > 0; i++)
                 {
-                    if (response.IsSuccessStatusCode)
-                    {
-                        string jsonString = await response.Content.ReadAsStringAsync();
-                        // ... Read the string.
-                        return JsonSerializer.Deserialize<List<Post>>(jsonString);
-                    }
-                    return new List<Post>();
+                    var post = _posts.Dequeue();
+                    _ids.Remove(post.Id);
+                    result.Add(post);
                 }
+                return result;
             }
-            catch (Exception)
+            else
             {
-                throw;
+                try
+                {
+                    using (HttpResponseMessage response = await httpClient.GetAsync($"https://kemono.su/api/v1/{service}/user/{id}?o={start}" + (!string.IsNullOrEmpty(search) ? $"&q={search}" : string.Empty)))
+                    using (HttpContent content = response.Content)
+                    {
+                        if (response.IsSuccessStatusCode)
+                        {
+                            string jsonString = await response.Content.ReadAsStringAsync();
+                            // ... Read the string.
+                            var resultList = JsonSerializer.Deserialize<List<Post>>(jsonString);
+
+                            var i = 0;
+
+                            if (!_postsIds.Equals(service + id, StringComparison.Ordinal))
+                            {
+                                _posts.Clear();
+                                _ids.Clear();
+                                _postsIds = service + id;
+                            }
+
+                            foreach (var post in resultList)
+                            {
+                                foreach (var item in post.Attachments)
+                                {
+                                    item.Service = post.Service;
+                                    item.User = post.User;
+                                }
+
+                                if (i >= _postsPerLoad && !_ids.Contains(post.Id))
+                                {
+                                    _ids.Add(post.Id);
+                                    _posts.Enqueue(post);
+                                }
+                                i++;
+                            }
+
+                            return resultList.Take(_postsPerLoad);
+                        }
+                         return Enumerable.Empty<Post>();
+                    }
+                
+                }
+                catch (Exception)
+                {
+                    throw;
+                }
             }
         }
 

+ 0 - 9
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Services/RegisteredServices.cs

@@ -1,9 +0,0 @@
-namespace VeloeAvaloniaKemonoPartyApp.Services
-{
-    public static class RegisteredServices
-    {
-        public static IOpenUrlService? OpenUrlService { get; set; }
-
-        public static IStorageService StorageService { get; set; } = new StorageService();
-    }
-}

+ 7 - 6
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp.csproj

@@ -4,8 +4,8 @@
     <Nullable>enable</Nullable>
     <LangVersion>latest</LangVersion>
     <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
-    <AssemblyVersion>1.0.0.350</AssemblyVersion>
-    <FileVersion>1.0.0.350</FileVersion>
+    <AssemblyVersion>1.0.0.396</AssemblyVersion>
+    <FileVersion>1.0.0.396</FileVersion>
   </PropertyGroup>
 
   
@@ -15,12 +15,13 @@
 
   <ItemGroup>
     <PackageReference Include="AngleSharp" Version="1.1.2" />
-    <PackageReference Include="Avalonia" Version="11.1.3" />
+    <PackageReference Include="Avalonia" Version="11.2.0-rc1" />
+    <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.4" />
     <PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.1.0.1" />
     <PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
-    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
-    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
-    <PackageReference Include="Avalonia.ReactiveUI" Version="11.1.3" />
+    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.0-rc1" />
+    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.0-rc1" />
+    <PackageReference Include="Avalonia.ReactiveUI" Version="11.2.0-rc1" />
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
     <PackageReference Include="HanumanInstitute.MvvmDialogs.Avalonia" Version="2.1.0" />

+ 3 - 1
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/AttachmentViewModel.cs

@@ -3,6 +3,8 @@ using VeloeAvaloniaKemonoPartyApp.Models;
 using System.Diagnostics;
 using System.Runtime.InteropServices;
 using System;
+using Splat;
+using VeloeAvaloniaKemonoPartyApp.Services;
 
 namespace VeloeAvaloniaKemonoPartyApp.ViewModels
 {
@@ -39,7 +41,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
                 return;
             }
 
-            Services.RegisteredServices.OpenUrlService?.OpenUrl(_attachment.Link);
+            Locator.Current.GetService<IOpenUrlService>()?.OpenUrl(_attachment.Link);
         }
     }
 }

+ 4 - 3
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorPostsViewModel.cs

@@ -1,4 +1,5 @@
 using Avalonia.Notification;
+using DynamicData.Binding;
 using HanumanInstitute.MvvmDialogs;
 using ReactiveUI;
 using System;
@@ -25,9 +26,9 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
         {
             _dialogService = dialogService;
             Close = ReactiveCommand.Create(CloseImpl);
-
+            
             this.WhenAnyValue(x => x.SearchText)
-                .Throttle(TimeSpan.FromMilliseconds(400))
+                .Throttle(TimeSpan.FromMilliseconds(800))
                 .ObserveOn(RxApp.MainThreadScheduler)
                 .Subscribe(InitPosts!);
         }
@@ -57,7 +58,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
             set 
             {
                 _creator = value;
-                InitPosts();
+                //InitPosts();
             } 
         }
 

+ 0 - 4
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorViewModel.cs

@@ -15,10 +15,6 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
             _creator = creator;
         }
 
-        public string Artist => _creator.Name;
-
-        public string Title => _creator.Service;
-
         public Creator Creator => _creator;
 
         private Bitmap? _cover;

+ 5 - 3
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorsViewModel.cs

@@ -28,7 +28,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
                 .ObserveOn(RxApp.MainThreadScheduler)
                 .Subscribe(DoSearch!);
 
-            this.WhenPropertyChanged(x => x.SelectedAlbum).Subscribe(async x => 
+            this.WhenPropertyChanged(x => x.SelectedCreator).Subscribe(async x => 
             {
                 if (x.Value is null) return;
                 try
@@ -52,9 +52,11 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
                 }
                 finally 
                 {
-                    Dispatcher.UIThread.Post(() => { SelectedAlbum = null; });
+                    Dispatcher.UIThread.Post(() => { SelectedCreator = null; });
                 }
             });
+
+            SearchText = "Rukis";
         }
 
         private string _searchText = string.Empty;
@@ -78,7 +80,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
 
         public ObservableCollection<CreatorViewModel> SearchResults { get; } = new();
 
-        public CreatorViewModel? SelectedAlbum
+        public CreatorViewModel? SelectedCreator
         {
             get => _selectedAlbum;
             set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);

+ 21 - 21
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/PostImageViewModel.cs

@@ -22,7 +22,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
             _dialogService = dialogService;
             _parent = parent;
         }
-
+        
         private Bitmap? _cover;
 
         public Bitmap? Cover
@@ -33,33 +33,33 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
 
         public async Task LoadAvatar()
         {
-            await using (var imageStream = await _attachment.LoadCoverBitmapAsync())
+            Cover = await Task.Run(async() =>
             {
-                if (imageStream is null) return;
+                await using (var imageStream = await _attachment.LoadCoverBitmapAsync())
+                {
+                if (imageStream is null) return null;
 
-                Cover = await Task.Run(() =>
+                Bitmap? bitmap;
+                try
                 {
-                    Bitmap? bitmap;
-                    try
+                    bitmap = new Bitmap(imageStream);
+                    if (App.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
                     {
-                        bitmap = new Bitmap(imageStream);
-                        if (App.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
-                        {
-                            var topLevel = TopLevel.GetTopLevel(lifetime.MainView);
+                        var topLevel = TopLevel.GetTopLevel(lifetime.MainView);
 
-                            if (bitmap.Size.Width > Math.Ceiling(topLevel.ClientSize.Width * topLevel.RenderScaling))
-                                bitmap = bitmap.CreateScaledBitmap(new Avalonia.PixelSize((int)Math.Ceiling(topLevel.ClientSize.Width*topLevel.RenderScaling), (int)Math.Ceiling(topLevel.ClientSize.Width*topLevel.RenderScaling / bitmap.Size.AspectRatio)));
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        bitmap = null;
+                        if (bitmap.Size.Width > Math.Ceiling(topLevel.ClientSize.Width * topLevel.RenderScaling))
+                            bitmap = bitmap.CreateScaledBitmap(new Avalonia.PixelSize((int)Math.Ceiling(topLevel.ClientSize.Width*topLevel.RenderScaling), (int)Math.Ceiling(topLevel.ClientSize.Width*topLevel.RenderScaling / bitmap.Size.AspectRatio)));
                     }
-                    return bitmap;
-                });
-            }
+                }
+                catch (Exception ex)
+                {
+                    bitmap = null;
+                }
+                return bitmap;
+                }
+            });
         }
-
+        
         public async Task OnImageClick(object sender, RoutedEventArgs e)
         {
             var vm = new ImageZoomViewModel(_attachment);

+ 2 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/PostViewModel.cs

@@ -69,6 +69,8 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
                 var attachment = new ExternalAttachment() { Url = element.Attributes.First(x => x.Name == "src").Value };
                 attachment.Path = Path.GetFileName(attachment.Url);
                 attachment.Name = Path.GetFileNameWithoutExtension(attachment.Path);
+                attachment.User = _post.User;
+                attachment.Service = _post.Service;
 
                 vm = new PostImageViewModel(_dialogService, _parent, attachment);
                 added = true;

+ 9 - 8
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorPostsView.axaml

@@ -4,6 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 			 xmlns:m="using:VeloeAvaloniaKemonoPartyApp.Models"
 			 xmlns:vm="clr-namespace:VeloeAvaloniaKemonoPartyApp.ViewModels"
+			 xmlns:views="using:VeloeAvaloniaKemonoPartyApp.Views"
 			 xmlns:notif="using:Avalonia.Notification.Controls"
              mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="854"
              x:Class="VeloeAvaloniaKemonoPartyApp.Views.CreatorPostsView"
@@ -14,7 +15,7 @@
          to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
 		<vm:CreatorPostsViewModel />
 	</Design.DataContext>
-	<Panel>
+	<Panel HorizontalAlignment="Stretch">
 		<notif:NotificationMessageContainer VerticalAlignment="Top" HorizontalAlignment="Stretch" Manager="{Binding Manager}" ZIndex="999"/>
 		<ScrollViewer VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
 			<StackPanel>
@@ -22,15 +23,15 @@
 					<TextBox Watermark="Search in posts..." Text="{Binding SearchText}"/>
 					<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}"/>
 				</StackPanel>
-				<ItemsControl 
+				<ItemsRepeater 
 					ItemsSource="{Binding Posts}"
 					Background="Transparent">
-					<ItemsControl.ItemsPanel>
-						<ItemsPanelTemplate>
-							<VirtualizingStackPanel />					
-						</ItemsPanelTemplate>
-					</ItemsControl.ItemsPanel>
-				</ItemsControl>
+					<ItemsRepeater.ItemTemplate>
+						<DataTemplate>
+							<views:PostView/>
+						</DataTemplate>
+					</ItemsRepeater.ItemTemplate>
+				</ItemsRepeater>
 				<Button 
 					Command="{Binding LoadMorePosts}"
 					HorizontalContentAlignment="Center"

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 10 - 5
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorView.axaml


+ 22 - 1
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorsView.axaml

@@ -20,7 +20,7 @@
 				<TextBox Watermark="Search for creators..." Text="{Binding SearchText}"/>
 				<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}"/>
 			</StackPanel>
-			<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" SelectionMode="Single"
+			<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedCreator}" SelectionMode="Single"
 						Background="Transparent" Margin="0 10" Gestures.PullGestureEnded="ReloadCreatorsSource">
 				<ListBox.ItemsPanel>
 					<ItemsPanelTemplate>
@@ -32,5 +32,26 @@
 				</ListBox.GestureRecognizers>
 			</ListBox>		
 		</DockPanel>
+		<Button 
+			VerticalAlignment="Bottom" 
+			HorizontalAlignment="Right" 
+			Margin="0 0 35 35" 
+			HorizontalContentAlignment="Center" 
+			VerticalContentAlignment="Center" 
+			Content="⚙️">
+			<Button.Styles>
+				<Style Selector="Button">
+					<Setter Property="Margin" Value="9,3,9,3"/>
+					<Setter Property="Width" Value="40"/>
+					<Setter Property="Height" Value="40"/>
+					<Setter Property="BorderThickness" Value="0"/>
+					<Setter Property="Padding" Value="6"/>
+					<Setter Property="CornerRadius" Value="20"/>
+				</Style>
+				<Style Selector="Button:pointerover /template/ ContentPresenter">
+					<Setter Property="Background" Value="#80e0e0e0"/>
+				</Style>
+			</Button.Styles>
+		</Button>
 	</Panel>
 </UserControl>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác