1
0

feat: Create repository select page

This commit is contained in:
Ca2didi 2025-03-30 02:52:46 +08:00
parent 6ddbf87221
commit fed8439796
20 changed files with 224 additions and 51 deletions

View File

@ -8,6 +8,7 @@
<entry key="Flawless.Client/Theme/ToggleSwitch.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HelloWindowView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/HomeView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/LoginView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainView.axaml" value="Flawless.Client/Flawless.Client.csproj" />
<entry key="Flawless.Client/Views/MainWindow.axaml" value="Flawless.Client/Flawless.Client.csproj" />

View File

@ -1,10 +1,12 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:semi="https://irihi.tech/semi"
xmlns:ursa="https://irihi.tech/ursa/themes/semi"
x:Class="Flawless.Client.App"
RequestedThemeVariant="Light">
<Application.Styles>
<semi:SemiTheme Locale="zh-cn"/>
<ursa:SemiTheme Locale="zh-cn"/>
</Application.Styles>
</Application>

View File

@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>

View File

@ -0,0 +1,27 @@
using System;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
namespace Flawless.Client.Models;
public record RepositoryHomePageModel(
string OwnerName,
string Name,
string Description,
bool IsArchived,
bool IsOwner,
string LatestCommitId)
{
public string FullName { get; } = $"{OwnerName}/{Name}";
public static RepositoryHomePageModel FromResponse(RepositoryInfoResponse r)
{
return new RepositoryHomePageModel(
r.OwnerUsername,
r.RepositoryName,
r.Description ?? String.Empty,
r.IsArchived,
r.Role == RepositoryRole.Owner,
r.LatestCommitId.ToString().Substring(0, 6));
}
}

View File

@ -0,0 +1,13 @@
using System;
using Avalonia.Threading;
namespace Flawless.Client;
public static class ObserverHelper
{
public static void SubscribeOnUIThread<T>(this IObservable<T> observer, Action<T> onNext)
{
var dispatcher = Dispatcher.UIThread;
observer.Subscribe(val => dispatcher.Invoke(() => onNext(val)));
}
}

View File

@ -1,17 +1,94 @@
using System;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.ReactiveUI;
using Flawless.Client.Models;
using Flawless.Client.Service;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Flawless.Client.ViewModels;
public class HomeViewModel : ViewModelBase, IRoutableViewModel
public partial class HomeViewModel : ViewModelBase, IRoutableViewModel
{
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
public IScreen HostScreen { get; }
public ObservableCollection<RepositoryHomePageModel> Repositories { get; } = new(new[]
{
new RepositoryHomePageModel(
"cardidi", "test1", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test2", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test3", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test4", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test5", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test6", "Abc", false, true, ""),
new RepositoryHomePageModel(
"cardidi", "test7", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test1", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test2", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test3", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test4", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test5", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test6", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test7", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test8", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test9", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test10", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test11", "Abc", false, true, ""),
new RepositoryHomePageModel(
"absyy", "test12", "Abc", false, true, ""),
});
[Reactive] private RepositoryHomePageModel? _selectedRepository;
[Reactive] private string _serverFriendlyName;
public HomeViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
Api.Current.ServerUrl.SubscribeOn(AvaloniaScheduler.Instance)
.Subscribe(v => ServerFriendlyName = v ?? "Unknown Server");
}
[ReactiveCommand]
private async Task RefreshRepositoriesAsync()
{
}
[ReactiveCommand]
private async Task CreateRepositoryAsync()
{
}
[ReactiveCommand]
private async Task OpenRepositoryAsync()
{
}
[ReactiveCommand]
private async Task DeleteRepositoryAsync()
{
}
}

View File

@ -10,16 +10,16 @@ using Refit;
namespace Flawless.Client.ViewModels;
public partial class LoginViewModel : ViewModelBase, IRoutableViewModel
public partial class LoginPageViewModel : ViewModelBase, IRoutableViewModel
{
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
public IScreen HostScreen { get; }
[Reactive] private string _username = String.Empty;
[Reactive] private string _username = "cardidi";
[Reactive] private string _password = String.Empty;
[Reactive] private string _password = "4453A2b33";
[Reactive(SetModifier = AccessModifier.Protected)] private string _issue = String.Empty;
@ -27,7 +27,7 @@ public partial class LoginViewModel : ViewModelBase, IRoutableViewModel
public IObservable<bool> CanRegister => Api.Current.Status.Select(s => s != null && s.AllowPublicRegister);
public LoginViewModel(IScreen hostScreen)
public LoginPageViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
@ -42,7 +42,7 @@ public partial class LoginViewModel : ViewModelBase, IRoutableViewModel
[ReactiveCommand(CanExecute = nameof(CanRegister))]
private void Register()
{
HostScreen.Router.Navigate.Execute(new RegisterViewModel(HostScreen));
HostScreen.Router.Navigate.Execute(new RegisterPageViewModel(HostScreen));
}
[ReactiveCommand(CanExecute = nameof(CanLogin))]

View File

@ -1,5 +1,6 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Threading;
using Flawless.Client.Service;
using ReactiveUI;
@ -8,25 +9,14 @@ namespace Flawless.Client.ViewModels;
public partial class MainWindowViewModel : ViewModelBase, IScreen
{
public RoutingState Router { get; } = new RoutingState();
public ReactiveCommand<Unit, IRoutableViewModel> GoBackCommand => Router.NavigateBack;
public MainWindowViewModel()
{
#pragma warning disable VSTHRD110
Api.Current.IsLoggedIn.Subscribe(login =>
{
if (login)
{
Console.WriteLine("Enter main page");
Router.Navigate.Execute(new LoginViewModel(this));
}
else
{
Console.WriteLine("Require login again");
Router.NavigateAndReset.Execute(new HomeViewModel(this));
}
});
Api.Current.IsLoggedIn.Where(x => x)
.SubscribeOnUIThread(_ => Router.Navigate.Execute(new HomeViewModel(this)));
Api.Current.IsLoggedIn.Where(x => !x)
.SubscribeOnUIThread(_ => Router.Navigate.Execute(new ServerConnectViewModel(this)));
#pragma warning restore VSTHRD110
}
}

View File

@ -10,7 +10,7 @@ using Refit;
namespace Flawless.Client.ViewModels;
public partial class RegisterViewModel : ViewModelBase, IRoutableViewModel
public partial class RegisterPageViewModel : ViewModelBase, IRoutableViewModel
{
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
@ -25,7 +25,7 @@ public partial class RegisterViewModel : ViewModelBase, IRoutableViewModel
[Reactive(SetModifier = AccessModifier.Protected)] private string _issue;
public RegisterViewModel(IScreen hostScreen)
public RegisterPageViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
}

View File

@ -26,7 +26,7 @@ public partial class ServerConnectViewModel : ViewModelBase, IScreen, IRoutableV
[ReactiveCommand]
private async ValueTask OpenRepoPageAsync()
{
if (Api.Current.RequireRefreshToken()) await Router.NavigateAndReset.Execute(new ServerSetupViewModel(this)).ToTask();
if (Api.Current.RequireRefreshToken()) await Router.NavigateAndReset.Execute(new ServerSetupPageViewModel(this)).ToTask();
}
public ServerConnectViewModel(IScreen hostScreen)

View File

@ -9,7 +9,7 @@ using Refit;
namespace Flawless.Client.ViewModels;
public partial class ServerSetupViewModel : ViewModelBase, IRoutableViewModel
public partial class ServerSetupPageViewModel : ViewModelBase, IRoutableViewModel
{
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
@ -22,7 +22,7 @@ public partial class ServerSetupViewModel : ViewModelBase, IRoutableViewModel
public IObservable<bool> CanSetHost { get; }
public ServerSetupViewModel(IScreen hostScreen)
public ServerSetupPageViewModel(IScreen hostScreen)
{
HostScreen = hostScreen;
@ -40,7 +40,7 @@ public partial class ServerSetupViewModel : ViewModelBase, IRoutableViewModel
{
Issue = string.Empty;
await Api.Current.SetGatewayAsync(Host);
HostScreen.Router.Navigate.Execute(new LoginViewModel(HostScreen));
HostScreen.Router.Navigate.Execute(new LoginPageViewModel(HostScreen));
}
catch (ApiException ex)
{

View File

@ -1,10 +1,73 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:ursa="https://irihi.tech/ursa"
xmlns:u="https://irihi.tech/ursa"
xmlns:semi="https://irihi.tech/semi"
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:vm="using:Flawless.Client.ViewModels"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="768"
x:DataType="vm:HomeViewModel"
x:Class="Flawless.Client.Views.HomeView">
<DockPanel Margin="50">
<StackPanel DockPanel.Dock="Top">
<Label Content="{Binding ServerFriendlyName, StringFormat='Server {0}'}" FontSize="18" FontWeight="400"></Label>
<Label Content="Repositories" FontSize="32" FontWeight="600"></Label>
<Rectangle Height="18"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"
Command="{Binding RefreshRepositoriesCommand}"/>
<u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Create"
Command="{Binding CreateRepositoryCommand}"/>
<u:DisableContainer IsEnabled="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Spacing="8">
<u:IconButton Icon="{StaticResource SemiIconFolderOpen}" Content="Open"
Command="{Binding OpenRepositoryCommand}"/>
<u:IconButton Classes="Danger" Icon="{StaticResource SemiIconDelete}" Content="Delete"
IsEnabled="{Binding SelectedRepository.IsOwner}"
Command="{Binding DeleteRepositoryCommand}"/>
</StackPanel>
</u:DisableContainer>
</StackPanel>
</StackPanel>
<Rectangle DockPanel.Dock="Top" Height="20"/>
<Grid ColumnDefinitions="*, 10, *" VerticalAlignment="Stretch">
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" AllowAutoHide="True">
<u:SelectionList ItemsSource="{Binding Repositories, Mode=TwoWay}"
SelectedItem="{Binding SelectedRepository, Mode=TwoWay}"
Margin="0, 0, 8, 0">
<u:SelectionList.ItemTemplate>
<DataTemplate>
<TextBlock Margin="0, 12" VerticalAlignment="Center" Text="{Binding FullName, Mode=OneWay}"/>
</DataTemplate>
</u:SelectionList.ItemTemplate>
</u:SelectionList>
</ScrollViewer>
<Border Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}" VerticalAlignment="Stretch">
<StackPanel VerticalAlignment="Stretch" Spacing="20">
<Label FontWeight="400" FontSize="24"
Content="{Binding SelectedRepository.Name, FallbackValue='Select a Repository'}"/>
<StackPanel Spacing="10" IsVisible="{Binding SelectedRepository, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel IsVisible="{Binding SelectedRepository.IsArchived, FallbackValue=False}"
Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconArchive}"/>
<Label Content="Archived"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconUser}"/>
<Label Content="{Binding SelectedRepository.OwnerName, FallbackValue='Owner'}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconActivity}"/>
<Label Content="{Binding SelectedRepository.LatestCommitId, FallbackValue='No Commit'}"/>
</StackPanel>
<ScrollViewer IsVisible="{Binding SelectedRepository}">
<Label Content="{Binding SelectedRepository.Description, FallbackValue='Description as below.'}"/>
</ScrollViewer>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</DockPanel>
</UserControl>

View File

@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Flawless.Client.ViewModels"
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540"
x:Class="Flawless.Client.Views.LoginView"
x:DataType="vm:LoginViewModel">
x:Class="Flawless.Client.Views.LoginPageView"
x:DataType="vm:LoginPageViewModel">
<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:LoginViewModel/>
<vm:LoginPageViewModel/>
</Design.DataContext>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="10">

View File

@ -7,9 +7,9 @@ using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views;
public partial class LoginView : ReactiveUrsaView<LoginViewModel>
public partial class LoginPageView : ReactiveUrsaView<LoginPageViewModel>
{
public LoginView()
public LoginPageView()
{
InitializeComponent();
this.WhenActivated(disposables => { });

View File

@ -9,9 +9,9 @@
xmlns:views="clr-namespace:Flawless.Client.Views"
x:DataType="vm:MainWindowViewModel"
Title="Flawless Version Control"
mc:Ignorable="d" d:DesignWidth="1024" d:DesignHeight="768"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="800"
x:CompileBindings="True"
CanResize="True" MinWidth="800" MinHeight="600"
CanResize="True" MinWidth="1024" MinHeight="768"
Icon="/Assets/avalonia-logo.ico">
<Design.DataContext>

View File

@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Flawless.Client.ViewModels"
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540"
x:Class="Flawless.Client.Views.RegisterView"
x:DataType="vm:RegisterViewModel">
x:Class="Flawless.Client.Views.RegisterPageView"
x:DataType="vm:RegisterPageViewModel">
<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:RegisterViewModel/>
<vm:RegisterPageViewModel/>
</Design.DataContext>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="10">

View File

@ -5,9 +5,9 @@ using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views;
public partial class RegisterView : ReactiveUrsaView<RegisterViewModel>
public partial class RegisterPageView : ReactiveUrsaView<RegisterPageViewModel>
{
public RegisterView()
public RegisterPageView()
{
InitializeComponent();
this.WhenActivated(disposables => { });

View File

@ -1,4 +1,5 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:ursa="https://irihi.tech/ursa"
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"
@ -11,9 +12,9 @@
<Border Classes="Shadow" Theme="{StaticResource CardBorder}" Width="400" Height="400">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" VerticalAlignment="Center" Spacing="18">
<Button Command="{Binding GoBackCommand}"
Theme="{DynamicResource BorderlessButton}"
Content="Back"/>
<ursa:IconButton
Command="{Binding GoBackCommand}"
Icon="{StaticResource SemiIconArrowLeft}"/>
<Label FontSize="24" Content="{Binding Title}"/>
</StackPanel>
<reactiveUi:RoutedViewHost Router="{Binding Router}">

View File

@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Flawless.Client.ViewModels"
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="540"
x:Class="Flawless.Client.Views.ServerSetupView"
x:DataType="vm:ServerSetupViewModel">
x:Class="Flawless.Client.Views.ServerSetupPageView"
x:DataType="vm:ServerSetupPageViewModel">
<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:ServerSetupViewModel/>
<vm:ServerSetupPageViewModel/>
</Design.DataContext>
<StackPanel Spacing="10" HorizontalAlignment="Stretch" VerticalAlignment="Center">

View File

@ -8,10 +8,10 @@ using Ursa.ReactiveUIExtension;
namespace Flawless.Client.Views;
public partial class ServerSetupView : ReactiveUrsaView<ServerSetupViewModel>
public partial class ServerSetupPageView : ReactiveUrsaView<ServerSetupPageViewModel>
{
public ServerSetupView()
public ServerSetupPageView()
{
InitializeComponent();
this.WhenActivated(disposables => { });