fix: 使用 CommunityToolkit.Mvvm
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.27.3" />
|
<PackageReference Include="Google.Protobuf" Version="3.27.3" />
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
|
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.65.0" PrivateAssets="All" />
|
<PackageReference Include="Grpc.Tools" Version="2.65.0" PrivateAssets="All" />
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace CloudPrint.Client.Infrastructure;
|
|
||||||
|
|
||||||
public sealed class AsyncRelayCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly Func<Task> _execute;
|
|
||||||
private readonly Func<bool>? _canExecute;
|
|
||||||
private bool _isExecuting;
|
|
||||||
|
|
||||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
|
||||||
{
|
|
||||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
|
||||||
_canExecute = canExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
|
||||||
|
|
||||||
public bool IsExecuting
|
|
||||||
{
|
|
||||||
get => _isExecuting;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (_isExecuting == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isExecuting = value;
|
|
||||||
RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExecute(object? parameter) => !IsExecuting && (_canExecute?.Invoke() ?? true);
|
|
||||||
|
|
||||||
public async void Execute(object? parameter)
|
|
||||||
{
|
|
||||||
if (!CanExecute(parameter))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsExecuting = true;
|
|
||||||
await _execute().ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsExecuting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace CloudPrint.Client.Infrastructure;
|
|
||||||
|
|
||||||
public abstract class ObservableObject : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
field = value;
|
|
||||||
OnPropertyChanged(propertyName);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace CloudPrint.Client.Infrastructure;
|
|
||||||
|
|
||||||
public sealed class RelayCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly Action<object?> _execute;
|
|
||||||
private readonly Predicate<object?>? _canExecute;
|
|
||||||
|
|
||||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
|
||||||
: this(_ => execute(), canExecute is null ? null : _ => canExecute())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
|
|
||||||
{
|
|
||||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
|
||||||
_canExecute = canExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
|
||||||
|
|
||||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
|
|
||||||
|
|
||||||
public void Execute(object? parameter) => _execute(parameter);
|
|
||||||
|
|
||||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
@@ -1,77 +1,45 @@
|
|||||||
using CloudPrint.Client.Infrastructure;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace CloudPrint.Client.Models;
|
namespace CloudPrint.Client.Models;
|
||||||
|
|
||||||
public sealed class ClientSettings : ObservableObject
|
public sealed partial class ClientSettings : ObservableObject
|
||||||
{
|
{
|
||||||
private string _serverAddress = "http://localhost:8080";
|
[ObservableProperty]
|
||||||
private string _apiKey = string.Empty;
|
private string serverAddress = "http://localhost:8080";
|
||||||
private string _adminUsername = string.Empty;
|
|
||||||
private string _adminAccessToken = string.Empty;
|
|
||||||
private string _storeName = "默认打印店";
|
|
||||||
private bool _allowInsecureGrpc = true;
|
|
||||||
private string _libraryRoot = string.Empty;
|
|
||||||
private string _printCacheRoot = string.Empty;
|
|
||||||
private int _maxUploadSizeMb = 50;
|
|
||||||
private int _heartbeatIntervalSeconds = 15;
|
|
||||||
|
|
||||||
public string ServerAddress
|
[ObservableProperty]
|
||||||
{
|
private string apiKey = string.Empty;
|
||||||
get => _serverAddress;
|
|
||||||
set => SetProperty(ref _serverAddress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ApiKey
|
[ObservableProperty]
|
||||||
{
|
private string adminUsername = string.Empty;
|
||||||
get => _apiKey;
|
|
||||||
set => SetProperty(ref _apiKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string AdminUsername
|
[ObservableProperty]
|
||||||
{
|
private string adminAccessToken = string.Empty;
|
||||||
get => _adminUsername;
|
|
||||||
set => SetProperty(ref _adminUsername, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string AdminAccessToken
|
[ObservableProperty]
|
||||||
{
|
private string storeName = "默认打印店";
|
||||||
get => _adminAccessToken;
|
|
||||||
set => SetProperty(ref _adminAccessToken, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StoreName
|
[ObservableProperty]
|
||||||
{
|
private bool allowInsecureGrpc = true;
|
||||||
get => _storeName;
|
|
||||||
set => SetProperty(ref _storeName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AllowInsecureGrpc
|
[ObservableProperty]
|
||||||
{
|
private string libraryRoot = string.Empty;
|
||||||
get => _allowInsecureGrpc;
|
|
||||||
set => SetProperty(ref _allowInsecureGrpc, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string LibraryRoot
|
[ObservableProperty]
|
||||||
{
|
private string printCacheRoot = string.Empty;
|
||||||
get => _libraryRoot;
|
|
||||||
set => SetProperty(ref _libraryRoot, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PrintCacheRoot
|
private int maxUploadSizeMb = 50;
|
||||||
{
|
private int heartbeatIntervalSeconds = 15;
|
||||||
get => _printCacheRoot;
|
|
||||||
set => SetProperty(ref _printCacheRoot, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int MaxUploadSizeMb
|
public int MaxUploadSizeMb
|
||||||
{
|
{
|
||||||
get => _maxUploadSizeMb;
|
get => maxUploadSizeMb;
|
||||||
set => SetProperty(ref _maxUploadSizeMb, Math.Clamp(value, 1, 1024));
|
set => SetProperty(ref maxUploadSizeMb, Math.Clamp(value, 1, 1024));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int HeartbeatIntervalSeconds
|
public int HeartbeatIntervalSeconds
|
||||||
{
|
{
|
||||||
get => _heartbeatIntervalSeconds;
|
get => heartbeatIntervalSeconds;
|
||||||
set => SetProperty(ref _heartbeatIntervalSeconds, Math.Clamp(value, 5, 300));
|
set => SetProperty(ref heartbeatIntervalSeconds, Math.Clamp(value, 5, 300));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
using CloudPrint.Client.Infrastructure;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace CloudPrint.Client.Models;
|
namespace CloudPrint.Client.Models;
|
||||||
|
|
||||||
public sealed class CloudPrintDocument : ObservableObject
|
public sealed partial class CloudPrintDocument : ObservableObject
|
||||||
{
|
{
|
||||||
private string _title = string.Empty;
|
[ObservableProperty]
|
||||||
private string _description = string.Empty;
|
private string title = string.Empty;
|
||||||
private string _categoryName = "未分类";
|
|
||||||
private bool _isUploaded;
|
[ObservableProperty]
|
||||||
private string _syncStatus = "本地";
|
private string description = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string categoryName = "未分类";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isUploaded;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string syncStatus = "本地";
|
||||||
|
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string FileName { get; init; } = string.Empty;
|
public string FileName { get; init; } = string.Empty;
|
||||||
@@ -21,36 +30,6 @@ public sealed class CloudPrintDocument : ObservableObject
|
|||||||
public int PageCount { get; init; }
|
public int PageCount { get; init; }
|
||||||
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
||||||
|
|
||||||
public string Title
|
|
||||||
{
|
|
||||||
get => _title;
|
|
||||||
set => SetProperty(ref _title, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Description
|
|
||||||
{
|
|
||||||
get => _description;
|
|
||||||
set => SetProperty(ref _description, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CategoryName
|
|
||||||
{
|
|
||||||
get => _categoryName;
|
|
||||||
set => SetProperty(ref _categoryName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsUploaded
|
|
||||||
{
|
|
||||||
get => _isUploaded;
|
|
||||||
set => SetProperty(ref _isUploaded, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SyncStatus
|
|
||||||
{
|
|
||||||
get => _syncStatus;
|
|
||||||
set => SetProperty(ref _syncStatus, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FileSizeText
|
public string FileSizeText
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using CloudPrint.Client.Infrastructure;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace CloudPrint.Client.Models;
|
namespace CloudPrint.Client.Models;
|
||||||
|
|
||||||
@@ -11,13 +11,22 @@ public enum PrintJobStatus
|
|||||||
Cancelled
|
Cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PrintJob : ObservableObject
|
public sealed partial class PrintJob : ObservableObject
|
||||||
{
|
{
|
||||||
private PrintJobStatus _status = PrintJobStatus.Pending;
|
[ObservableProperty]
|
||||||
private int _progress;
|
private int? windowsJobId;
|
||||||
private string _errorMessage = string.Empty;
|
|
||||||
private int? _windowsJobId;
|
[ObservableProperty]
|
||||||
private string _windowsJobStatus = string.Empty;
|
private string windowsJobStatus = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(StatusText))]
|
||||||
|
private PrintJobStatus status = PrintJobStatus.Pending;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string errorMessage = string.Empty;
|
||||||
|
|
||||||
|
private int progress;
|
||||||
|
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string JobNo { get; init; } = string.Empty;
|
public string JobNo { get; init; } = string.Empty;
|
||||||
@@ -34,40 +43,10 @@ public sealed class PrintJob : ObservableObject
|
|||||||
public DateTime? StartedAt { get; set; }
|
public DateTime? StartedAt { get; set; }
|
||||||
public DateTime? CompletedAt { get; set; }
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
|
||||||
public int? WindowsJobId
|
|
||||||
{
|
|
||||||
get => _windowsJobId;
|
|
||||||
set => SetProperty(ref _windowsJobId, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WindowsJobStatus
|
|
||||||
{
|
|
||||||
get => _windowsJobStatus;
|
|
||||||
set => SetProperty(ref _windowsJobStatus, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrintJobStatus Status
|
|
||||||
{
|
|
||||||
get => _status;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _status, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(StatusText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Progress
|
public int Progress
|
||||||
{
|
{
|
||||||
get => _progress;
|
get => progress;
|
||||||
set => SetProperty(ref _progress, Math.Clamp(value, 0, 100));
|
set => SetProperty(ref progress, Math.Clamp(value, 0, 100));
|
||||||
}
|
|
||||||
|
|
||||||
public string ErrorMessage
|
|
||||||
{
|
|
||||||
get => _errorMessage;
|
|
||||||
set => SetProperty(ref _errorMessage, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StatusText => Status switch
|
public string StatusText => Status switch
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using CloudPrint.Client.Infrastructure;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace CloudPrint.Client.Models;
|
namespace CloudPrint.Client.Models;
|
||||||
|
|
||||||
@@ -11,11 +11,20 @@ public enum PrinterStatus
|
|||||||
Unknown = 99
|
Unknown = 99
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PrinterInfo : ObservableObject
|
public sealed partial class PrinterInfo : ObservableObject
|
||||||
{
|
{
|
||||||
private PrinterStatus _status;
|
[ObservableProperty]
|
||||||
private int _queueCount;
|
[NotifyPropertyChangedFor(nameof(DisplayText))]
|
||||||
private string _statusMessage = string.Empty;
|
private int queueCount;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(DisplayText))]
|
||||||
|
private string statusMessage = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(StatusText))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(DisplayText))]
|
||||||
|
private PrinterStatus status;
|
||||||
|
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
@@ -24,40 +33,11 @@ public sealed class PrinterInfo : ObservableObject
|
|||||||
public string ApiKey { get; init; } = string.Empty;
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
public DateTime? LastHeartbeat { get; set; }
|
public DateTime? LastHeartbeat { get; set; }
|
||||||
|
|
||||||
public int QueueCount
|
partial void OnQueueCountChanged(int value)
|
||||||
{
|
{
|
||||||
get => _queueCount;
|
if (value < 0)
|
||||||
set
|
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _queueCount, Math.Max(0, value)))
|
QueueCount = 0;
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(DisplayText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get => _statusMessage;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _statusMessage, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(DisplayText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrinterStatus Status
|
|
||||||
{
|
|
||||||
get => _status;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _status, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(StatusText));
|
|
||||||
OnPropertyChanged(nameof(DisplayText));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,16 @@ public sealed class GrpcService : IPrintBackendClient
|
|||||||
throw new ArgumentException("API Key 不能为空", nameof(apiKey));
|
throw new ArgumentException("API Key 不能为空", nameof(apiKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
_apiKey = apiKey;
|
|
||||||
if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && AllowInsecureGrpc)
|
if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && AllowInsecureGrpc)
|
||||||
{
|
{
|
||||||
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_channel?.Dispose();
|
ResetConnectionState(clearCredentials: true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_apiKey = apiKey;
|
||||||
_channel = GrpcChannel.ForAddress(address);
|
_channel = GrpcChannel.ForAddress(address);
|
||||||
_printClient = new Cloudprint.PrintService.PrintServiceClient(_channel);
|
_printClient = new Cloudprint.PrintService.PrintServiceClient(_channel);
|
||||||
_libraryClient = new Cloudprint.LibraryService.LibraryServiceClient(_channel);
|
_libraryClient = new Cloudprint.LibraryService.LibraryServiceClient(_channel);
|
||||||
@@ -67,6 +70,12 @@ public sealed class GrpcService : IPrintBackendClient
|
|||||||
IsConnected = true;
|
IsConnected = true;
|
||||||
Log(AppLogLevel.Success, $"已连接 gRPC 服务:{address}");
|
Log(AppLogLevel.Success, $"已连接 gRPC 服务:{address}");
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ResetConnectionState(clearCredentials: true);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AdminSession> LoginAdminAsync(string username, string password, CancellationToken cancellationToken = default)
|
public async Task<AdminSession> LoginAdminAsync(string username, string password, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -240,14 +249,7 @@ public sealed class GrpcService : IPrintBackendClient
|
|||||||
|
|
||||||
public void Disconnect()
|
public void Disconnect()
|
||||||
{
|
{
|
||||||
_subscriptionCts?.Cancel();
|
ResetConnectionState(clearCredentials: true);
|
||||||
IsConnected = false;
|
|
||||||
_channel?.Dispose();
|
|
||||||
_channel = null;
|
|
||||||
_printClient = null;
|
|
||||||
_libraryClient = null;
|
|
||||||
_fileClient = null;
|
|
||||||
_adminClient = null;
|
|
||||||
Log(AppLogLevel.Warning, "已断开 gRPC 连接");
|
Log(AppLogLevel.Warning, "已断开 gRPC 连接");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +285,28 @@ public sealed class GrpcService : IPrintBackendClient
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log(AppLogLevel.Error, $"打印任务订阅中断:{ex.Message}");
|
Log(AppLogLevel.Error, $"打印任务订阅中断:{ex.Message}");
|
||||||
|
ResetConnectionState(clearCredentials: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetConnectionState(bool clearCredentials)
|
||||||
|
{
|
||||||
|
_subscriptionCts?.Cancel();
|
||||||
|
_subscriptionCts?.Dispose();
|
||||||
|
_subscriptionCts = null;
|
||||||
|
_channel?.Dispose();
|
||||||
|
_channel = null;
|
||||||
|
_printClient = null;
|
||||||
|
_libraryClient = null;
|
||||||
|
_fileClient = null;
|
||||||
|
_adminClient = null;
|
||||||
IsConnected = false;
|
IsConnected = false;
|
||||||
|
PrinterId = 0;
|
||||||
|
Token = string.Empty;
|
||||||
|
|
||||||
|
if (clearCredentials)
|
||||||
|
{
|
||||||
|
_apiKey = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CloudPrint.Client.Infrastructure;
|
|
||||||
using CloudPrint.Client.Models;
|
using CloudPrint.Client.Models;
|
||||||
using CloudPrint.Client.Services;
|
using CloudPrint.Client.Services;
|
||||||
using CloudPrint.Client.Services.Abstractions;
|
using CloudPrint.Client.Services.Abstractions;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace CloudPrint.Client.ViewModels;
|
namespace CloudPrint.Client.ViewModels;
|
||||||
|
|
||||||
public sealed class LibraryViewModel : ObservableObject
|
public sealed partial class LibraryViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IFileService _fileService;
|
private readonly IFileService _fileService;
|
||||||
private readonly IPrintBackendClient _backendClient;
|
private readonly IPrintBackendClient _backendClient;
|
||||||
private readonly ClientSettings _settings;
|
private readonly ClientSettings _settings;
|
||||||
private LibraryCategory? _selectedCategory;
|
|
||||||
private CloudPrintDocument? _selectedDocument;
|
[ObservableProperty]
|
||||||
private string _statusMessage = "文库就绪";
|
private LibraryCategory? selectedCategory;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(RemoveDocumentCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SyncSelectedCommand))]
|
||||||
|
private CloudPrintDocument? selectedDocument;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string statusMessage = "文库就绪";
|
||||||
|
|
||||||
public LibraryViewModel()
|
public LibraryViewModel()
|
||||||
: this(new FileService(), new GrpcService(), new ClientSettings())
|
: this(new FileService(), new GrpcService(), new ClientSettings())
|
||||||
@@ -33,51 +42,18 @@ public sealed class LibraryViewModel : ObservableObject
|
|||||||
Categories.Add(new LibraryCategory { Id = 4, Name = "其他文件", Icon = "📁", SortOrder = 99 });
|
Categories.Add(new LibraryCategory { Id = 4, Name = "其他文件", Icon = "📁", SortOrder = 99 });
|
||||||
|
|
||||||
SelectedCategory = Categories.FirstOrDefault();
|
SelectedCategory = Categories.FirstOrDefault();
|
||||||
AddDocumentCommand = new RelayCommand(AddDocument);
|
|
||||||
RemoveDocumentCommand = new RelayCommand(RemoveSelectedDocument, () => SelectedDocument is not null);
|
|
||||||
SyncSelectedCommand = new AsyncRelayCommand(SyncSelectedAsync, () => SelectedDocument is not null && _backendClient.IsConnected);
|
|
||||||
SyncAllCommand = new AsyncRelayCommand(SyncAllAsync, () => Documents.Any(document => !document.IsUploaded) && _backendClient.IsConnected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<LibraryCategory> Categories { get; } = new();
|
public ObservableCollection<LibraryCategory> Categories { get; } = new();
|
||||||
public ObservableCollection<CloudPrintDocument> Documents { get; } = new();
|
public ObservableCollection<CloudPrintDocument> Documents { get; } = new();
|
||||||
|
|
||||||
public LibraryCategory? SelectedCategory
|
|
||||||
{
|
|
||||||
get => _selectedCategory;
|
|
||||||
set => SetProperty(ref _selectedCategory, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CloudPrintDocument? SelectedDocument
|
|
||||||
{
|
|
||||||
get => _selectedDocument;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedDocument, value))
|
|
||||||
{
|
|
||||||
RemoveDocumentCommand.RaiseCanExecuteChanged();
|
|
||||||
SyncSelectedCommand.RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get => _statusMessage;
|
|
||||||
private set => SetProperty(ref _statusMessage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelayCommand AddDocumentCommand { get; }
|
|
||||||
public RelayCommand RemoveDocumentCommand { get; }
|
|
||||||
public AsyncRelayCommand SyncSelectedCommand { get; }
|
|
||||||
public AsyncRelayCommand SyncAllCommand { get; }
|
|
||||||
|
|
||||||
public void RefreshCommandState()
|
public void RefreshCommandState()
|
||||||
{
|
{
|
||||||
SyncSelectedCommand.RaiseCanExecuteChanged();
|
SyncSelectedCommand.NotifyCanExecuteChanged();
|
||||||
SyncAllCommand.RaiseCanExecuteChanged();
|
SyncAllCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void AddDocument()
|
private void AddDocument()
|
||||||
{
|
{
|
||||||
var dialog = new Microsoft.Win32.OpenFileDialog
|
var dialog = new Microsoft.Win32.OpenFileDialog
|
||||||
@@ -114,7 +90,10 @@ public sealed class LibraryViewModel : ObservableObject
|
|||||||
RefreshCommandState();
|
RefreshCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSelectedDocument()
|
private bool CanRemoveDocument() => SelectedDocument is not null;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanRemoveDocument))]
|
||||||
|
private void RemoveDocument()
|
||||||
{
|
{
|
||||||
if (SelectedDocument is null)
|
if (SelectedDocument is null)
|
||||||
{
|
{
|
||||||
@@ -128,6 +107,9 @@ public sealed class LibraryViewModel : ObservableObject
|
|||||||
RefreshCommandState();
|
RefreshCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanSyncSelected() => SelectedDocument is not null && _backendClient.IsConnected;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSyncSelected))]
|
||||||
private async Task SyncSelectedAsync()
|
private async Task SyncSelectedAsync()
|
||||||
{
|
{
|
||||||
if (SelectedDocument is null)
|
if (SelectedDocument is null)
|
||||||
@@ -140,6 +122,9 @@ public sealed class LibraryViewModel : ObservableObject
|
|||||||
RefreshCommandState();
|
RefreshCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanSyncAll() => Documents.Any(document => !document.IsUploaded) && _backendClient.IsConnected;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSyncAll))]
|
||||||
private async Task SyncAllAsync()
|
private async Task SyncAllAsync()
|
||||||
{
|
{
|
||||||
var pendingDocuments = Documents.Where(document => !document.IsUploaded).ToList();
|
var pendingDocuments = Documents.Where(document => !document.IsUploaded).ToList();
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ using CloudPrint.Client.Infrastructure;
|
|||||||
using CloudPrint.Client.Models;
|
using CloudPrint.Client.Models;
|
||||||
using CloudPrint.Client.Services;
|
using CloudPrint.Client.Services;
|
||||||
using CloudPrint.Client.Services.Abstractions;
|
using CloudPrint.Client.Services.Abstractions;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
using ObservableObject = CommunityToolkit.Mvvm.ComponentModel.ObservableObject;
|
||||||
|
|
||||||
namespace CloudPrint.Client.ViewModels;
|
namespace CloudPrint.Client.ViewModels;
|
||||||
|
|
||||||
public sealed class MainViewModel : ObservableObject
|
public sealed partial class MainViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private const int MaxReconnectAttempts = 5;
|
private const int MaxReconnectAttempts = 5;
|
||||||
private static readonly TimeSpan SubscriptionWatchInterval = TimeSpan.FromSeconds(5);
|
private static readonly TimeSpan SubscriptionWatchInterval = TimeSpan.FromSeconds(5);
|
||||||
@@ -28,16 +31,42 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
private CancellationTokenSource? _queueCts;
|
private CancellationTokenSource? _queueCts;
|
||||||
private CancellationTokenSource? _processingCts;
|
private CancellationTokenSource? _processingCts;
|
||||||
private Task? _queueWorker;
|
private Task? _queueWorker;
|
||||||
private PrintJob? _selectedJob;
|
|
||||||
private PrintJob? _processingJob;
|
private PrintJob? _processingJob;
|
||||||
private PrinterInfo? _selectedPrinter;
|
private PrinterInfo? _subscribedPrinter;
|
||||||
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
|
||||||
private string _lastMessage = "已就绪";
|
|
||||||
private bool _isReconnecting;
|
private bool _isReconnecting;
|
||||||
private bool _isManualDisconnect = true;
|
private bool _isManualDisconnect = true;
|
||||||
private bool _autoProcessQueue = true;
|
|
||||||
private bool _isQueuePaused;
|
[ObservableProperty]
|
||||||
private bool _isQueueProcessing;
|
[NotifyCanExecuteChangedFor(nameof(CancelSelectedJobCommand))]
|
||||||
|
private PrintJob? selectedJob;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanSubscribe))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SubscribeCommand))]
|
||||||
|
private PrinterInfo? selectedPrinter;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ConnectionStatus))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanSubscribe))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SubscribeCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(DisconnectCommand))]
|
||||||
|
private ConnectionState connectionState = ConnectionState.Disconnected;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(QueueStatusText))]
|
||||||
|
private bool autoProcessQueue = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(QueueStatusText))]
|
||||||
|
private bool isQueuePaused;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(QueueStatusText))]
|
||||||
|
private bool isQueueProcessing;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string lastMessage = "已就绪";
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
: this(new PrintService(), new FileService(), new GrpcService(), new WebSocketService(), new JsonSettingsService(), new WpfUiDispatcher(), new FileAppLogger())
|
: this(new PrintService(), new FileService(), new GrpcService(), new WebSocketService(), new JsonSettingsService(), new WpfUiDispatcher(), new FileAppLogger())
|
||||||
@@ -70,17 +99,6 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
_backendClient.LogReceived += (_, entry) => AddLog(entry);
|
_backendClient.LogReceived += (_, entry) => AddLog(entry);
|
||||||
_webSocketService.LogReceived += (_, entry) => AddLog(entry);
|
_webSocketService.LogReceived += (_, entry) => AddLog(entry);
|
||||||
|
|
||||||
RefreshPrintersCommand = new RelayCommand(RefreshPrinters);
|
|
||||||
ConnectCommand = new AsyncRelayCommand(ConnectAsync, () => SelectedPrinter is not null);
|
|
||||||
SubscribeCommand = new AsyncRelayCommand(SubscribeAsync, () => CanSubscribe);
|
|
||||||
ProcessSelectedJobCommand = new AsyncRelayCommand(ProcessSelectedJobAsync, () => Jobs.Any(job => job.Status == PrintJobStatus.Pending) && !IsQueueProcessing);
|
|
||||||
CancelSelectedJobCommand = new AsyncRelayCommand(CancelSelectedJobAsync, () => CanCancelSelectedJob);
|
|
||||||
PauseQueueCommand = new RelayCommand(PauseQueue, () => AutoProcessQueue && !IsQueuePaused);
|
|
||||||
ResumeQueueCommand = new RelayCommand(ResumeQueue, () => AutoProcessQueue && IsQueuePaused);
|
|
||||||
DisconnectCommand = new RelayCommand(Disconnect, () => _backendClient.IsConnected || _webSocketService.IsConnected);
|
|
||||||
SaveSettingsCommand = new RelayCommand(SaveSettings);
|
|
||||||
ClearLogsCommand = new RelayCommand(ClearLogs);
|
|
||||||
|
|
||||||
RefreshPrinters();
|
RefreshPrinters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,47 +109,6 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
public LibraryViewModel Library { get; }
|
public LibraryViewModel Library { get; }
|
||||||
public SettingsViewModel SettingsPanel { get; }
|
public SettingsViewModel SettingsPanel { get; }
|
||||||
|
|
||||||
public PrintJob? SelectedJob
|
|
||||||
{
|
|
||||||
get => _selectedJob;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedJob, value))
|
|
||||||
{
|
|
||||||
CancelSelectedJobCommand.RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrinterInfo? SelectedPrinter
|
|
||||||
{
|
|
||||||
get => _selectedPrinter;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedPrinter, value))
|
|
||||||
{
|
|
||||||
(ConnectCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
|
||||||
(SubscribeCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConnectionState ConnectionState
|
|
||||||
{
|
|
||||||
get => _connectionState;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _connectionState, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(ConnectionStatus));
|
|
||||||
OnPropertyChanged(nameof(CanSubscribe));
|
|
||||||
SubscribeCommand.RaiseCanExecuteChanged();
|
|
||||||
DisconnectCommand.RaiseCanExecuteChanged();
|
|
||||||
Library.RefreshCommandState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ConnectionStatus => ConnectionState switch
|
public string ConnectionStatus => ConnectionState switch
|
||||||
{
|
{
|
||||||
ConnectionState.Connecting => "连接中",
|
ConnectionState.Connecting => "连接中",
|
||||||
@@ -145,49 +122,6 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
public bool CanSubscribe => SelectedPrinter is not null && _backendClient.IsConnected;
|
public bool CanSubscribe => SelectedPrinter is not null && _backendClient.IsConnected;
|
||||||
public bool CanCancelSelectedJob => SelectedJob is { Status: PrintJobStatus.Pending or PrintJobStatus.Printing };
|
public bool CanCancelSelectedJob => SelectedJob is { Status: PrintJobStatus.Pending or PrintJobStatus.Printing };
|
||||||
|
|
||||||
public bool AutoProcessQueue
|
|
||||||
{
|
|
||||||
get => _autoProcessQueue;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _autoProcessQueue, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(QueueStatusText));
|
|
||||||
RaiseQueueCommandStates();
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
StartQueueProcessingIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsQueuePaused
|
|
||||||
{
|
|
||||||
get => _isQueuePaused;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _isQueuePaused, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(QueueStatusText));
|
|
||||||
RaiseQueueCommandStates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsQueueProcessing
|
|
||||||
{
|
|
||||||
get => _isQueueProcessing;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _isQueueProcessing, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(QueueStatusText));
|
|
||||||
RaiseQueueCommandStates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string QueueStatusText => AutoProcessQueue switch
|
public string QueueStatusText => AutoProcessQueue switch
|
||||||
{
|
{
|
||||||
false => "自动队列已关闭,可手动处理下一任务",
|
false => "自动队列已关闭,可手动处理下一任务",
|
||||||
@@ -196,23 +130,31 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
_ => "自动队列已开启,等待新任务"
|
_ => "自动队列已开启,等待新任务"
|
||||||
};
|
};
|
||||||
|
|
||||||
public string LastMessage
|
partial void OnConnectionStateChanged(ConnectionState value)
|
||||||
{
|
{
|
||||||
get => _lastMessage;
|
Library.RefreshCommandState();
|
||||||
private set => SetProperty(ref _lastMessage, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RelayCommand RefreshPrintersCommand { get; }
|
partial void OnAutoProcessQueueChanged(bool value)
|
||||||
public AsyncRelayCommand ConnectCommand { get; }
|
{
|
||||||
public AsyncRelayCommand SubscribeCommand { get; }
|
RaiseQueueCommandStates();
|
||||||
public AsyncRelayCommand ProcessSelectedJobCommand { get; }
|
if (value)
|
||||||
public AsyncRelayCommand CancelSelectedJobCommand { get; }
|
{
|
||||||
public RelayCommand PauseQueueCommand { get; }
|
StartQueueProcessingIfNeeded();
|
||||||
public RelayCommand ResumeQueueCommand { get; }
|
}
|
||||||
public RelayCommand DisconnectCommand { get; }
|
}
|
||||||
public RelayCommand SaveSettingsCommand { get; }
|
|
||||||
public RelayCommand ClearLogsCommand { get; }
|
|
||||||
|
|
||||||
|
partial void OnIsQueuePausedChanged(bool value)
|
||||||
|
{
|
||||||
|
RaiseQueueCommandStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsQueueProcessingChanged(bool value)
|
||||||
|
{
|
||||||
|
RaiseQueueCommandStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void RefreshPrinters()
|
private void RefreshPrinters()
|
||||||
{
|
{
|
||||||
Printers.Clear();
|
Printers.Clear();
|
||||||
@@ -225,6 +167,9 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
AddLog(AppLogLevel.Info, $"发现 {Printers.Count} 台打印机");
|
AddLog(AppLogLevel.Info, $"发现 {Printers.Count} 台打印机");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanConnect() => SelectedPrinter is not null;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanConnect))]
|
||||||
private async Task ConnectAsync()
|
private async Task ConnectAsync()
|
||||||
{
|
{
|
||||||
if (SelectedPrinter is null)
|
if (SelectedPrinter is null)
|
||||||
@@ -246,16 +191,20 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
StartHeartbeatLoop();
|
StartHeartbeatLoop();
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Connected;
|
ConnectionState = ConnectionState.Connected;
|
||||||
DisconnectCommand.RaiseCanExecuteChanged();
|
DisconnectCommand.NotifyCanExecuteChanged();
|
||||||
Library.RefreshCommandState();
|
Library.RefreshCommandState();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
ResetConnectionsAfterFailure();
|
||||||
ConnectionState = ConnectionState.Error;
|
ConnectionState = ConnectionState.Error;
|
||||||
AddLog(AppLogEntry.FromException($"连接失败:{ex.Message}", ex));
|
AddLog(AppLogEntry.FromException($"连接失败:{ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanSubscribeCommand() => CanSubscribe;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSubscribeCommand))]
|
||||||
private async Task SubscribeAsync()
|
private async Task SubscribeAsync()
|
||||||
{
|
{
|
||||||
if (SelectedPrinter is null)
|
if (SelectedPrinter is null)
|
||||||
@@ -268,19 +217,24 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
ConnectionState = ConnectionState.Subscribing;
|
ConnectionState = ConnectionState.Subscribing;
|
||||||
await _backendClient.SubscribePrintJobsAsync(SelectedPrinter).ConfigureAwait(true);
|
await _backendClient.SubscribePrintJobsAsync(SelectedPrinter).ConfigureAwait(true);
|
||||||
|
_subscribedPrinter = SelectedPrinter;
|
||||||
ConnectionState = ConnectionState.Subscribed;
|
ConnectionState = ConnectionState.Subscribed;
|
||||||
StartSubscriptionWatchLoop();
|
StartSubscriptionWatchLoop();
|
||||||
ProcessSelectedJobCommand.RaiseCanExecuteChanged();
|
ProcessSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
Library.RefreshCommandState();
|
Library.RefreshCommandState();
|
||||||
StartQueueProcessingIfNeeded();
|
StartQueueProcessingIfNeeded();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
ResetConnectionsAfterFailure();
|
||||||
ConnectionState = ConnectionState.Error;
|
ConnectionState = ConnectionState.Error;
|
||||||
AddLog(AppLogEntry.FromException($"订阅失败:{ex.Message}", ex));
|
AddLog(AppLogEntry.FromException($"订阅失败:{ex.Message}", ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanProcessSelectedJob() => Jobs.Any(job => job.Status == PrintJobStatus.Pending) && !IsQueueProcessing;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanProcessSelectedJob))]
|
||||||
private async Task ProcessSelectedJobAsync()
|
private async Task ProcessSelectedJobAsync()
|
||||||
{
|
{
|
||||||
var job = GetNextPendingJob(preferSelected: true);
|
var job = GetNextPendingJob(preferSelected: true);
|
||||||
@@ -309,6 +263,9 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanCancelSelectedJobCommand() => CanCancelSelectedJob;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanCancelSelectedJobCommand))]
|
||||||
private async Task CancelSelectedJobAsync()
|
private async Task CancelSelectedJobAsync()
|
||||||
{
|
{
|
||||||
if (SelectedJob is null)
|
if (SelectedJob is null)
|
||||||
@@ -335,8 +292,8 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ProcessSelectedJobCommand.RaiseCanExecuteChanged();
|
ProcessSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
CancelSelectedJobCommand.RaiseCanExecuteChanged();
|
CancelSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +308,9 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnsureJobPrinter(e.Job);
|
||||||
Jobs.Insert(0, e.Job);
|
Jobs.Insert(0, e.Job);
|
||||||
ProcessSelectedJobCommand.RaiseCanExecuteChanged();
|
ProcessSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDuplicate)
|
if (isDuplicate)
|
||||||
@@ -364,6 +322,7 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
StartQueueProcessingIfNeeded();
|
StartQueueProcessingIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void SaveSettings()
|
private void SaveSettings()
|
||||||
{
|
{
|
||||||
ConfigureBackendMode();
|
ConfigureBackendMode();
|
||||||
@@ -379,6 +338,22 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ResetConnectionsAfterFailure()
|
||||||
|
{
|
||||||
|
_heartbeatCts?.Cancel();
|
||||||
|
_subscriptionWatchCts?.Cancel();
|
||||||
|
_queueCts?.Cancel();
|
||||||
|
_processingCts?.Cancel();
|
||||||
|
_subscribedPrinter = null;
|
||||||
|
_backendClient.Disconnect();
|
||||||
|
_ = _webSocketService.DisconnectAsync();
|
||||||
|
NotifyConnectionCommandStates();
|
||||||
|
Library.RefreshCommandState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanDisconnect() => _backendClient.IsConnected || _webSocketService.IsConnected;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanDisconnect))]
|
||||||
private void Disconnect()
|
private void Disconnect()
|
||||||
{
|
{
|
||||||
_isManualDisconnect = true;
|
_isManualDisconnect = true;
|
||||||
@@ -386,19 +361,27 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
_subscriptionWatchCts?.Cancel();
|
_subscriptionWatchCts?.Cancel();
|
||||||
_queueCts?.Cancel();
|
_queueCts?.Cancel();
|
||||||
_processingCts?.Cancel();
|
_processingCts?.Cancel();
|
||||||
|
_subscribedPrinter = null;
|
||||||
_backendClient.Disconnect();
|
_backendClient.Disconnect();
|
||||||
_ = _webSocketService.DisconnectAsync();
|
_ = _webSocketService.DisconnectAsync();
|
||||||
ConnectionState = ConnectionState.Disconnected;
|
ConnectionState = ConnectionState.Disconnected;
|
||||||
AddLog(AppLogLevel.Warning, "客户端已断开连接");
|
AddLog(AppLogLevel.Warning, "客户端已断开连接");
|
||||||
|
NotifyConnectionCommandStates();
|
||||||
Library.RefreshCommandState();
|
Library.RefreshCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanPauseQueue() => AutoProcessQueue && !IsQueuePaused;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanPauseQueue))]
|
||||||
private void PauseQueue()
|
private void PauseQueue()
|
||||||
{
|
{
|
||||||
IsQueuePaused = true;
|
IsQueuePaused = true;
|
||||||
AddLog(AppLogLevel.Warning, "自动队列已暂停,当前任务完成后停止处理后续任务");
|
AddLog(AppLogLevel.Warning, "自动队列已暂停,当前任务完成后停止处理后续任务");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanResumeQueue() => AutoProcessQueue && IsQueuePaused;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanResumeQueue))]
|
||||||
private void ResumeQueue()
|
private void ResumeQueue()
|
||||||
{
|
{
|
||||||
IsQueuePaused = false;
|
IsQueuePaused = false;
|
||||||
@@ -521,10 +504,19 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_uiDispatcher.Invoke(() =>
|
_uiDispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
ProcessSelectedJobCommand.RaiseCanExecuteChanged();
|
ProcessSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
CancelSelectedJobCommand.RaiseCanExecuteChanged();
|
CancelSelectedJobCommand.NotifyCanExecuteChanged();
|
||||||
PauseQueueCommand.RaiseCanExecuteChanged();
|
PauseQueueCommand.NotifyCanExecuteChanged();
|
||||||
ResumeQueueCommand.RaiseCanExecuteChanged();
|
ResumeQueueCommand.NotifyCanExecuteChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyConnectionCommandStates()
|
||||||
|
{
|
||||||
|
_uiDispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
SubscribeCommand.NotifyCanExecuteChanged();
|
||||||
|
DisconnectCommand.NotifyCanExecuteChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,6 +634,7 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
await _backendClient.RegisterPrinterAsync(SelectedPrinter, Settings.ApiKey, cancellationToken).ConfigureAwait(false);
|
await _backendClient.RegisterPrinterAsync(SelectedPrinter, Settings.ApiKey, cancellationToken).ConfigureAwait(false);
|
||||||
await _backendClient.SendHeartbeatAsync(SelectedPrinter, cancellationToken).ConfigureAwait(false);
|
await _backendClient.SendHeartbeatAsync(SelectedPrinter, cancellationToken).ConfigureAwait(false);
|
||||||
await _backendClient.SubscribePrintJobsAsync(SelectedPrinter, cancellationToken).ConfigureAwait(false);
|
await _backendClient.SubscribePrintJobsAsync(SelectedPrinter, cancellationToken).ConfigureAwait(false);
|
||||||
|
_subscribedPrinter = SelectedPrinter;
|
||||||
await _webSocketService.ConnectAsync(new Uri(ToWebSocketAddress(Settings.ServerAddress)), cancellationToken).ConfigureAwait(false);
|
await _webSocketService.ConnectAsync(new Uri(ToWebSocketAddress(Settings.ServerAddress)), cancellationToken).ConfigureAwait(false);
|
||||||
_uiDispatcher.Invoke(() => ConnectionState = ConnectionState.Subscribed);
|
_uiDispatcher.Invoke(() => ConnectionState = ConnectionState.Subscribed);
|
||||||
AddLog(AppLogLevel.Success, "自动重连成功,任务订阅已恢复");
|
AddLog(AppLogLevel.Success, "自动重连成功,任务订阅已恢复");
|
||||||
@@ -672,6 +665,7 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void ClearLogs()
|
private void ClearLogs()
|
||||||
{
|
{
|
||||||
Logs.Clear();
|
Logs.Clear();
|
||||||
@@ -724,6 +718,23 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
return !Jobs.Any(existing => GetJobKey(existing).Equals(key, StringComparison.OrdinalIgnoreCase));
|
return !Jobs.Any(existing => GetJobKey(existing).Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureJobPrinter(PrintJob job)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(job.PrinterName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var printer = _subscribedPrinter ?? SelectedPrinter;
|
||||||
|
if (printer is null)
|
||||||
|
{
|
||||||
|
AddLog(AppLogLevel.Error, $"打印任务缺少本机打印机:{GetJobDisplayName(job)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.PrinterName = printer.Name;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetJobKey(PrintJob job)
|
private static string GetJobKey(PrintJob job)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(job.JobNo))
|
if (!string.IsNullOrWhiteSpace(job.JobNo))
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
using CloudPrint.Client.Models;
|
using CloudPrint.Client.Models;
|
||||||
using CloudPrint.Client.Infrastructure;
|
|
||||||
using CloudPrint.Client.Services;
|
using CloudPrint.Client.Services;
|
||||||
using CloudPrint.Client.Services.Abstractions;
|
using CloudPrint.Client.Services.Abstractions;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace CloudPrint.Client.ViewModels;
|
namespace CloudPrint.Client.ViewModels;
|
||||||
|
|
||||||
public sealed class SettingsViewModel : ObservableObject
|
public sealed partial class SettingsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IPrintBackendClient _backendClient;
|
private readonly IPrintBackendClient _backendClient;
|
||||||
private readonly IAppLogger _appLogger;
|
private readonly IAppLogger _appLogger;
|
||||||
private string _statusMessage = "设置就绪";
|
|
||||||
private string _adminPassword = string.Empty;
|
[ObservableProperty]
|
||||||
|
private string statusMessage = "设置就绪";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string adminPassword = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(AdminLoginStatus))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(LogoutAdminCommand))]
|
||||||
|
private bool isAdminLoggedIn;
|
||||||
|
|
||||||
public SettingsViewModel()
|
public SettingsViewModel()
|
||||||
: this(new JsonSettingsService().Load(), new JsonSettingsService(), new GrpcService(), new FileAppLogger())
|
: this(new JsonSettingsService().Load(), new JsonSettingsService(), new GrpcService(), new FileAppLogger())
|
||||||
@@ -25,11 +35,6 @@ public sealed class SettingsViewModel : ObservableObject
|
|||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_backendClient = backendClient;
|
_backendClient = backendClient;
|
||||||
_appLogger = appLogger;
|
_appLogger = appLogger;
|
||||||
SaveCommand = new RelayCommand(Save);
|
|
||||||
SelectLibraryRootCommand = new RelayCommand(SelectLibraryRoot);
|
|
||||||
LoginAdminCommand = new AsyncRelayCommand(LoginAdminAsync);
|
|
||||||
LogoutAdminCommand = new RelayCommand(LogoutAdmin, () => IsAdminLoggedIn);
|
|
||||||
OpenLogDirectoryCommand = new RelayCommand(OpenLogDirectory);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Settings.AdminAccessToken))
|
if (!string.IsNullOrWhiteSpace(Settings.AdminAccessToken))
|
||||||
{
|
{
|
||||||
@@ -40,47 +45,18 @@ public sealed class SettingsViewModel : ObservableObject
|
|||||||
|
|
||||||
public ClientSettings Settings { get; }
|
public ClientSettings Settings { get; }
|
||||||
|
|
||||||
public string StatusMessage
|
|
||||||
{
|
|
||||||
get => _statusMessage;
|
|
||||||
private set => SetProperty(ref _statusMessage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelayCommand SaveCommand { get; }
|
|
||||||
public RelayCommand SelectLibraryRootCommand { get; }
|
|
||||||
public AsyncRelayCommand LoginAdminCommand { get; }
|
|
||||||
public RelayCommand LogoutAdminCommand { get; }
|
|
||||||
public RelayCommand OpenLogDirectoryCommand { get; }
|
|
||||||
public string LogDirectory => _appLogger.LogDirectory;
|
public string LogDirectory => _appLogger.LogDirectory;
|
||||||
|
|
||||||
private bool _isAdminLoggedIn;
|
|
||||||
public bool IsAdminLoggedIn
|
|
||||||
{
|
|
||||||
get => _isAdminLoggedIn;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _isAdminLoggedIn, value))
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(AdminLoginStatus));
|
|
||||||
LogoutAdminCommand.RaiseCanExecuteChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string AdminLoginStatus => IsAdminLoggedIn ? $"已登录:{Settings.AdminUsername}" : "未登录";
|
public string AdminLoginStatus => IsAdminLoggedIn ? $"已登录:{Settings.AdminUsername}" : "未登录";
|
||||||
|
|
||||||
public string AdminPassword
|
[RelayCommand]
|
||||||
{
|
|
||||||
get => _adminPassword;
|
|
||||||
set => SetProperty(ref _adminPassword, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
_settingsService.Save(Settings);
|
_settingsService.Save(Settings);
|
||||||
StatusMessage = "设置已保存";
|
StatusMessage = "设置已保存";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private async Task LoginAdminAsync()
|
private async Task LoginAdminAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -106,6 +82,9 @@ public sealed class SettingsViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanLogoutAdmin() => IsAdminLoggedIn;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanLogoutAdmin))]
|
||||||
private void LogoutAdmin()
|
private void LogoutAdmin()
|
||||||
{
|
{
|
||||||
_backendClient.LogoutAdmin();
|
_backendClient.LogoutAdmin();
|
||||||
@@ -124,6 +103,7 @@ public sealed class SettingsViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void SelectLibraryRoot()
|
private void SelectLibraryRoot()
|
||||||
{
|
{
|
||||||
using var dialog = new System.Windows.Forms.FolderBrowserDialog
|
using var dialog = new System.Windows.Forms.FolderBrowserDialog
|
||||||
@@ -142,6 +122,7 @@ public sealed class SettingsViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
private void OpenLogDirectory()
|
private void OpenLogDirectory()
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_appLogger.LogDirectory);
|
Directory.CreateDirectory(_appLogger.LogDirectory);
|
||||||
|
|||||||
Reference in New Issue
Block a user