Browse Source

added creator posts view

Veloe 1 year ago
parent
commit
9b79cf391e

+ 2 - 2
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/App.axaml.cs

@@ -19,14 +19,14 @@ namespace VeloeAvaloniaKemonoPartyApp
             {
                 desktop.MainWindow = new MainWindow
                 {
-                    DataContext = new MainViewModel()
+                    DataContext = new MainWindowViewModel()
                 };
             }
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
             {
                 singleViewPlatform.MainView = new MainView
                 {
-                    DataContext = new MainViewModel()
+                    DataContext = new MainWindowViewModel()
                 };
             }
 

+ 11 - 10
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Creator.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net;
 using System.Net.Http;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -21,20 +22,13 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
 
         public static IList<Creator> Source = new List<Creator>(66000);
 
-        public Creator(string name, string service, string id)
-        {
-            this.name = name;
-            this.service = service;
-            this.id = id;
-        }
-
         public static async Task<IEnumerable<Creator>> SearchAsync(string searchTerm)
         {
             //TODO create disk cache
             if (Source.Count == 0)
                 Source = await s_httpClient.GetCreatorsList();
 
-            return Source.Where(x=>x.name.StartsWith(searchTerm)).Take(50);
+            return Source.Where(x=>x.name.StartsWith(searchTerm,System.StringComparison.OrdinalIgnoreCase)).Take(50);
         }
 
         private static KemonoHttpClient s_httpClient = new();
@@ -48,8 +42,15 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
             }
             else
             {
-                var data = await s_httpClient.httpClient.GetByteArrayAsync(Icon);
-                return new MemoryStream(data);
+                try
+                {
+                    var data = await s_httpClient.httpClient.GetByteArrayAsync(Icon);
+                    return new MemoryStream(data);
+                }
+                catch (System.Net.Http.HttpRequestException ex)
+                {
+                    return new MemoryStream();
+                }
             }
         }
 

+ 87 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Models/Post.cs

@@ -1,5 +1,11 @@
 using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text.Json;
 using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using VeloeKemonoPartyApp.Services;
 
 
 namespace VeloeAvaloniaKemonoPartyApp.Models
@@ -30,6 +36,87 @@ namespace VeloeAvaloniaKemonoPartyApp.Models
         public string Title { get; set; }
         [JsonPropertyName("user")]
         public string User { get; set; }
+
+        public static async Task<IEnumerable<Post>> InitPostsAsync(Creator creator)
+        {
+            return await s_httpClient.GetPostsList(creator.id,creator.service,0);
+        }
+
+        public static async Task<IEnumerable<Post>> LoadPostsAsync(Creator creator, int start)
+        {
+            return await s_httpClient.GetPostsList(creator.id, creator.service, start);
+        }
+
+        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 async Task SaveAsync()
+        {
+            if (!Directory.Exists("./Cache"))
+            {
+                Directory.CreateDirectory("./Cache");
+            }
+
+            using (var fs = System.IO.File.OpenWrite(CachePath))
+            {
+                await SaveToStreamAsync(this, fs);
+            }
+        }
+
+        public Stream SaveCoverBitmapStream()
+        {
+            return System.IO.File.OpenWrite(CachePath + ".bmp");
+        }
+
+        private static async Task SaveToStreamAsync(Post data, Stream stream)
+        {
+            await JsonSerializer.SerializeAsync(stream, data).ConfigureAwait(false);
+        }
+
+        public static async Task<Post> LoadFromStream(Stream stream)
+        {
+            return (await JsonSerializer.DeserializeAsync<Post>(stream).ConfigureAwait(false))!;
+        }
+
+        public static async Task<IEnumerable<Post>> LoadCachedAsync()
+        {
+            if (!Directory.Exists("./Cache"))
+            {
+                Directory.CreateDirectory("./Cache");
+            }
+
+            var results = new List<Post>();
+
+            foreach (var file in Directory.EnumerateFiles("./Cache"))
+            {
+                if (!string.IsNullOrWhiteSpace(new DirectoryInfo(file).Extension)) continue;
+
+                await using var fs = System.IO.File.OpenRead(file);
+                results.Add(await Post.LoadFromStream(fs).ConfigureAwait(false));
+            }
+
+            return results;
+        }
     }
 
     public class Attachment

+ 75 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/CreatorPostsViewModel.cs

@@ -0,0 +1,75 @@
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using VeloeAvaloniaKemonoPartyApp.Models;
+
+namespace VeloeAvaloniaKemonoPartyApp.ViewModels
+{
+    public class CreatorPostsViewModel : ViewModelBase
+    {
+        private Creator _creator;
+
+        public ObservableCollection<PostViewModel> Posts { get; } = new();
+
+        public CreatorPostsViewModel(Creator creator) 
+        {
+            _creator = creator;
+            InitPosts();
+        }
+
+        private CancellationTokenSource? _cancellationTokenSource;
+
+        private bool _isBusy = false;
+        public bool IsBusy
+        {
+            get => _isBusy;
+            set => this.RaiseAndSetIfChanged(ref _isBusy, value);
+        }
+
+        private async void InitPosts()
+        {
+            _cancellationTokenSource?.Cancel();
+            _cancellationTokenSource = new CancellationTokenSource();
+            var cancellationToken = _cancellationTokenSource.Token;
+
+            IsBusy = true;
+            Posts.Clear();
+
+            if (_creator is not null)
+            {
+                var posts = await Post.InitPostsAsync(_creator);
+
+                foreach (var post in posts)
+                {
+                    var vm = new PostViewModel(post);
+                    Posts.Add(vm);
+                }
+
+                if (!cancellationToken.IsCancellationRequested)
+                {
+                    LoadAvatars(cancellationToken);
+                }
+            }
+
+            IsBusy = false;
+        }
+
+        private async void LoadAvatars(CancellationToken cancellationToken)
+        {
+            foreach (var post in Posts.ToList())
+            {
+                await post.LoadAvatar();
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    return;
+                }
+            }
+        }
+    }
+}

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

@@ -22,6 +22,8 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
 
         public string Title => _creator.updated.ToString();
 
+        public Creator Creator => _creator;
+
         private Bitmap? _cover;
 
         public Bitmap? Cover
@@ -30,7 +32,7 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
             private set => this.RaiseAndSetIfChanged(ref _cover, value);
         }
 
-        public async Task LoadCover()
+        public async Task LoadAvatar()
         {
             await using (var imageStream = await _creator.LoadCoverBitmapAsync())
             {

+ 11 - 9
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/MainViewModel.cs

@@ -1,7 +1,9 @@
-using ReactiveUI;
+using Avalonia.Controls;
+using ReactiveUI;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Text;
@@ -29,10 +31,10 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
                 .Subscribe(DoSearch!);
         }
 
-        private string? _searchText;
+        private string _searchText = string.Empty;
         private bool _isBusy = false;
 
-        public string? SearchText
+        public string SearchText
         {
             get => _searchText;
             set => this.RaiseAndSetIfChanged(ref _searchText, value);
@@ -44,11 +46,11 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
             set => this.RaiseAndSetIfChanged(ref _isBusy, value);
         }
 
-        private Creator? _selectedAlbum;
+        private CreatorViewModel _selectedAlbum;
 
         public ObservableCollection<CreatorViewModel> SearchResults { get; } = new();
 
-        public Creator? SelectedAlbum
+        public CreatorViewModel? SelectedAlbum
         {
             get => _selectedAlbum;
             set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);
@@ -75,18 +77,18 @@ namespace VeloeAvaloniaKemonoPartyApp.ViewModels
 
                 if (!cancellationToken.IsCancellationRequested)
                 {
-                    LoadCovers(cancellationToken);
+                    LoadAvatars(cancellationToken);
                 }
             }
 
             IsBusy = false;
         }
 
-        private async void LoadCovers(CancellationToken cancellationToken)
+        private async void LoadAvatars(CancellationToken cancellationToken)
         {
-            foreach (var album in SearchResults.ToList())
+            foreach (var creator in SearchResults.ToList())
             {
-                await album.LoadCover();
+                await creator.LoadAvatar();
 
                 if (cancellationToken.IsCancellationRequested)
                 {

+ 42 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/ViewModels/MainWindowViewModel.cs

@@ -0,0 +1,42 @@
+using Avalonia.Controls;
+using DynamicData.Binding;
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace VeloeAvaloniaKemonoPartyApp.ViewModels
+{
+    public class MainWindowViewModel : ViewModelBase
+    {
+        private ViewModelBase _contentViewModel;
+
+        public MainWindowViewModel()
+        {
+            ActualMainViewModel = new MainViewModel();
+            _contentViewModel = ActualMainViewModel;
+
+            ActualMainViewModel.WhenAnyValue(x => x.SelectedAlbum).Subscribe(AddItem!);
+        }
+
+        public MainViewModel ActualMainViewModel { get; }
+
+        public ViewModelBase ContentViewModel
+        {
+            get => _contentViewModel;
+            private set => this.RaiseAndSetIfChanged(ref _contentViewModel, value);
+        }
+
+        public void AddItem(CreatorViewModel creatorViewModel)
+        {
+            if (creatorViewModel == null) return;
+
+            Debug.WriteLine("Call new view");
+
+            ContentViewModel = new CreatorPostsViewModel(creatorViewModel.Creator);
+        }
+    }
+}

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

@@ -0,0 +1,73 @@
+using Avalonia.Media.Imaging;
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using VeloeAvaloniaKemonoPartyApp.Models;
+
+namespace VeloeAvaloniaKemonoPartyApp.ViewModels
+{
+    public class PostViewModel : ViewModelBase
+    {
+        private readonly Post _post;
+
+        public PostViewModel(Post post)
+        {
+            _post = post;
+        }
+
+        public string Artist => _post.Content;
+
+        public string Title => _post.Title;
+
+        private Bitmap? _cover;
+
+        public Bitmap? Cover
+        {
+            get => _cover;
+            private set => this.RaiseAndSetIfChanged(ref _cover, value);
+        }
+
+        public async Task LoadAvatar()
+        {
+            if (_post.Attachments.Count == 0) return;
+
+            await using (var imageStream = await _post.LoadCoverBitmapAsync())
+            {
+                Cover = await Task.Run(() =>
+                {
+                    Bitmap? bitmap;
+                    try
+                    {
+                        bitmap = new Bitmap(imageStream);
+                    }
+                    catch (Exception ex)
+                    {
+                        bitmap = null;
+                    }
+                    return bitmap;
+                });
+            }
+        }
+
+        public async Task SaveToDiskAsync()
+        {
+            await _post.SaveAsync();
+
+            if (Cover != null)
+            {
+                var bitmap = Cover;
+
+                await Task.Run(() =>
+                {
+                    using (var fs = _post.SaveCoverBitmapStream())
+                    {
+                        bitmap.Save(fs);
+                    }
+                });
+            }
+        }
+    }
+}

+ 27 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorPostsView.axaml

@@ -0,0 +1,27 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+			 xmlns:m="using:VeloeAvaloniaKemonoPartyApp.Models"
+			 xmlns:vm="clr-namespace:VeloeAvaloniaKemonoPartyApp.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="VeloeAvaloniaKemonoPartyApp.Views.CreatorPostsView"
+			 x:DataType="vm:CreatorPostsViewModel">
+	<Design.DataContext>
+		<!-- This only sets the DataContext for the previewer in an IDE,
+         to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
+		<vm:CreatorPostsViewModel />
+	</Design.DataContext>
+	<DockPanel>
+		<ScrollViewer>
+			<ItemsControl ItemsSource="{Binding Posts}"
+					 Background="Transparent" Margin="0 20">
+				<ItemsControl.ItemsPanel>
+					<ItemsPanelTemplate>
+						<VirtualizingStackPanel />					
+					</ItemsPanelTemplate>
+				</ItemsControl.ItemsPanel>
+			</ItemsControl>
+		</ScrollViewer>
+	</DockPanel>
+</UserControl>

+ 12 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorPostsView.axaml.cs

@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace VeloeAvaloniaKemonoPartyApp.Views
+{
+    public partial class CreatorPostsView : UserControl
+    {
+        public CreatorPostsView()
+        {
+            InitializeComponent();
+        }
+    }
+}

File diff suppressed because it is too large
+ 0 - 1
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/CreatorView.axaml


+ 2 - 3
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/MainView.axaml

@@ -5,7 +5,7 @@
              xmlns:m="using:VeloeAvaloniaKemonoPartyApp.Models"
 			 xmlns:vm="clr-namespace:VeloeAvaloniaKemonoPartyApp.ViewModels"
 			 xmlns:asyncImageLoader="using:AsyncImageLoader"
-             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+			 mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="VeloeAvaloniaKemonoPartyApp.Views.MainView"
              x:DataType="vm:MainViewModel">
   <Design.DataContext>
@@ -18,14 +18,13 @@
 			<TextBox Watermark="Search for creators..." Text="{Binding SearchText}"/>
 			<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}"/>
 		</StackPanel>
-		<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}"
+		<ListBox ItemsSource="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" SelectionMode="Single"
 				 Background="Transparent" Margin="0 20">
 			<ListBox.ItemsPanel>
 				<ItemsPanelTemplate>
 					<WrapPanel />
 				</ItemsPanelTemplate>
 			</ListBox.ItemsPanel>
-
 		</ListBox>
 	</DockPanel>
   

+ 3 - 2
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/MainWindow.axaml

@@ -8,6 +8,7 @@
         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
         x:Class="VeloeAvaloniaKemonoPartyApp.Views.MainWindow"
         Icon="/Assets/avalonia-logo.ico"
-        Title="VeloeAvaloniaKemonoPartyApp">
-        <views:MainView />
+        Title="VeloeAvaloniaKemonoPartyApp"
+		x:DataType="vm:MainWindowViewModel"
+		Content="{Binding ContentViewModel}">
 </Window>

File diff suppressed because it is too large
+ 13 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/PostView.axaml


+ 12 - 0
VeloeAvaloniaKemonoPartyApp/VeloeAvaloniaKemonoPartyApp/Views/PostView.axaml.cs

@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace VeloeAvaloniaKemonoPartyApp.Views
+{
+    public partial class PostView : UserControl
+    {
+        public PostView()
+        {
+            InitializeComponent();
+        }
+    }
+}

Some files were not shown because too many files changed in this diff