From a69e71b86b167a8e23055bb86cc3f2b8dd486469 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E4=BC=9F?= <421281095@qq.com>
Date: Thu, 10 Apr 2025 13:17:39 +0800
Subject: [PATCH] =?UTF-8?q?=20object=20=E4=B8=8B=E8=BD=BD=E5=8F=8A?=
=?UTF-8?q?=E9=83=A8=E9=97=A8=E4=BC=A0=E8=BE=93=E9=A1=B5=E9=9D=A2=E9=80=BB?=
=?UTF-8?q?=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Models/CustomCommand.cs | 25 ++++
Provider/SqlSugarConfig.cs | 2 +-
Services/MinioDownloadTask.cs | 155 +++++++++++++++++++
View/Send/DownControl.xaml | 217 ++++++++++++++++++++++++---
View/StatusToButtonConverter.cs | 22 +++
ViewModel/Loyout/LoyoutViewModel.cs | 43 +++---
ViewModel/Send/DownViewModel.cs | 222 ++++++++++++++++++++++++++--
ViewModel/Sync/SyncViewModel.cs | 37 ++---
minio.db | Bin 20480 -> 73728 bytes
9 files changed, 637 insertions(+), 86 deletions(-)
create mode 100644 Models/CustomCommand.cs
create mode 100644 Services/MinioDownloadTask.cs
create mode 100644 View/StatusToButtonConverter.cs
diff --git a/Models/CustomCommand.cs b/Models/CustomCommand.cs
new file mode 100644
index 0000000..0bce3bd
--- /dev/null
+++ b/Models/CustomCommand.cs
@@ -0,0 +1,25 @@
+using System.Windows.Input;
+
+namespace Hopetry.Models;
+
+public class CustomCommand : ICommand
+{
+ private readonly Action _execute;
+
+ public CustomCommand(Action execute)
+ {
+ _execute = execute;
+ }
+
+ public bool CanExecute(object parameter)
+ {
+ return true;
+ }
+
+ public event EventHandler CanExecuteChanged;
+
+ public void Execute(object parameter)
+ {
+ _execute();
+ }
+}
\ No newline at end of file
diff --git a/Provider/SqlSugarConfig.cs b/Provider/SqlSugarConfig.cs
index 11a0390..3982c46 100644
--- a/Provider/SqlSugarConfig.cs
+++ b/Provider/SqlSugarConfig.cs
@@ -14,7 +14,7 @@ namespace Hopetry.Provider
{
return new SqlSugarClient(new ConnectionConfig()
{
- ConnectionString = @"DataSource=E:\数据上传转存\sqlite\minio.db", // 数据库路径
+ ConnectionString = @"DataSource=minio.db", // 数据库路径
DbType = DbType.Sqlite, // 数据库类型
IsAutoCloseConnection = true, // 自动释放
InitKeyType = InitKeyType.Attribute // 从实体特性中读取主键信息
diff --git a/Services/MinioDownloadTask.cs b/Services/MinioDownloadTask.cs
new file mode 100644
index 0000000..5a2f398
--- /dev/null
+++ b/Services/MinioDownloadTask.cs
@@ -0,0 +1,155 @@
+using System.Windows;
+using System.Windows.Input;
+using HeBianGu.Base.WpfBase;
+using Hopetry.Models;
+using SqlSugar;
+
+namespace Hopetry.Services;
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.ComponentModel;
+using Minio;
+
+[SugarTable(TableName = "download_task")]
+public class MinioDownloadTask : INotifyPropertyChanged
+{
+ private readonly MinioService _minio;
+
+ ///
+ ///
+ ///
+ private CancellationTokenSource _cts;
+
+ ///
+ ///
+ ///
+ private ManualResetEventSlim _pauseEvent = new(true);
+
+ // 任务属性(绑定到UI)
+ [SugarColumn(ColumnName = "task_id")] public int TaskId { get; set; }
+
+ [SugarColumn(ColumnName = "file_name")]
+ public string FileName { get; set; }
+
+ [SugarColumn(ColumnName = "bucket_name")]
+ public string Bucket { get; set; }
+
+ [SugarColumn(ColumnName = "object_key")]
+ public string ObjectKey { get; set; }
+
+ [SugarColumn(ColumnName = "total_size")]
+ public long TotalSize { get; private set; }
+
+ [SugarColumn(ColumnName = "downloaded")]
+ public long Downloaded { get; private set; }
+
+ [SugarColumn(ColumnName = "file_path")]
+ public string FilePath { get; set; }
+
+ [SugarColumn(ColumnName = "status")]
+ public string Status { get; private set; } = "等待中";
+
+ public string Progress
+ {
+ get { return TotalSize == 0 ? "0%" : $"{(Downloaded * 100 / TotalSize):0.0}%"; }
+ }
+
+ // 命令
+ public ICommand PauseCommand { get; }
+ public ICommand CancelCommand { get; }
+
+ public MinioDownloadTask()
+ {
+ PauseCommand = new CustomCommand(OnPause);
+ CancelCommand = new CustomCommand(OnCancel);
+ }
+ public MinioDownloadTask(MinioService minio, string bucket, string objectKey)
+ {
+ _minio = minio;
+ Bucket = bucket;
+ ObjectKey = objectKey;
+ TaskId = Interlocked.Increment(ref _globalId);
+ FileName = Path.GetFileName(objectKey);
+
+ PauseCommand = new CustomCommand(OnPause);
+ CancelCommand = new CustomCommand(OnCancel);
+ }
+
+ ///
+ /// 下载
+ ///
+ ///
+ public async Task StartDownload(string savePath)
+ {
+ Status = "初始化";
+ _cts = new CancellationTokenSource();
+ var tmpPath = $"{savePath}.download";
+
+ try
+ {
+ // 断点续传初始化
+ if (File.Exists(tmpPath))
+ Downloaded = new FileInfo(tmpPath).Length;
+
+ // 获取文件信息
+ var stat = await _minio.GetObjectMetadata(Bucket, ObjectKey);
+ TotalSize = stat.Size;
+
+ if (Downloaded >= TotalSize)
+ {
+ Status = "已完成";
+ return;
+ }
+
+ // todo 下载逻辑
+ Status = "已完成";
+ }
+ catch (OperationCanceledException)
+ {
+ Status = "已取消";
+ }
+ catch (Exception ex)
+ {
+ Status = $"错误:{ex.Message}";
+ }
+ finally
+ {
+ _pauseEvent.Dispose();
+ OnPropertyChanged(nameof(Status));
+ }
+ }
+
+ private void OnPause()
+ {
+ if (Status == "下载中")
+ {
+ _pauseEvent.Reset();
+ Status = "已暂停";
+ }
+ else if (Status == "已暂停")
+ {
+ _pauseEvent.Set();
+ Status = "下载中";
+ }
+
+ OnPropertyChanged(nameof(Status));
+ }
+
+ private void OnCancel()
+ {
+ _cts?.Cancel();
+ Status = "已取消";
+ OnPropertyChanged(nameof(Status));
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string propertyName)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private static int _globalId;
+}
\ No newline at end of file
diff --git a/View/Send/DownControl.xaml b/View/Send/DownControl.xaml
index 6c076f8..4b96d1e 100644
--- a/View/Send/DownControl.xaml
+++ b/View/Send/DownControl.xaml
@@ -18,12 +18,17 @@
-
-
-
-
-
+
+
+
@@ -51,7 +56,7 @@
HorizontalAlignment="Left"
Style="{DynamicResource {x:Static h:TextBlockKeys.Default}}"
Text="{Binding FileName}" />
-
+
-
-
-
-
-
-
-
-
-
+ Value="11">
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"暂停",
+ "已暂停" => "恢复",
+ _ => "开始"
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/ViewModel/Loyout/LoyoutViewModel.cs b/ViewModel/Loyout/LoyoutViewModel.cs
index d338cfc..126af25 100644
--- a/ViewModel/Loyout/LoyoutViewModel.cs
+++ b/ViewModel/Loyout/LoyoutViewModel.cs
@@ -1,23 +1,22 @@
-using HeBianGu.Base.WpfBase;
-using HeBianGu.Service.Mvc;
-using Hopetry.Services;
-using Microsoft.Win32;
-using Minio.DataModel.Args;
-using Minio.DataModel;
-using System;
-using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Timers;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
-using Minio;
-using Microsoft.Extensions.Configuration;
-using Hopetry.ViewModel.Send;
-using System.IO;
-using System.Security.AccessControl;
-using System.Diagnostics;
-using Microsoft.WindowsAPICodePack.Dialogs;
+using HeBianGu.Base.WpfBase;
+using HeBianGu.Service.Mvc;
using Hopetry.Models;
-using Yitter.IdGenerator;
+using Hopetry.Services;
+using Hopetry.ViewModel.Send;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Win32;
+using Microsoft.WindowsAPICodePack.Dialogs;
+using Minio;
+using Minio.DataModel;
+using Minio.DataModel.Args;
+using Timer = System.Timers.Timer;
namespace HeBianGu.App.Disk
{
@@ -103,7 +102,7 @@ namespace HeBianGu.App.Disk
private SendViewModel _sendViewModel;
private IConfiguration config;
private SemaphoreSlim _semaphore = new SemaphoreSlim(5);
- private System.Timers.Timer _progressTimer;
+ private Timer _progressTimer;
private bool _isTimerRunning = false;
private object _timerLock = new object();
private bool _isUploading = false; // 新增:标记是否有上传任务正在运行
@@ -123,7 +122,7 @@ namespace HeBianGu.App.Disk
UploadCommand = new AsyncRelayCommand(async () => await UploadFile());
UploadCommand1 = new AsyncRelayCommand(async () => await UploadFile1());
// 初始化Timer
- _progressTimer = new System.Timers.Timer(1000);
+ _progressTimer = new Timer(1000);
_progressTimer.Elapsed += UpdateProgress;
_sendViewModel.UpLoadItems.CollectionChanged += (s, e) =>
{
@@ -131,7 +130,7 @@ namespace HeBianGu.App.Disk
};
}
- private void UpdateProgress(object sender, System.Timers.ElapsedEventArgs e)
+ private void UpdateProgress(object sender, ElapsedEventArgs e)
{
lock (_timerLock)
{
@@ -174,7 +173,7 @@ namespace HeBianGu.App.Disk
private async Task UploadFile()
{
- var openFileDialog = new Microsoft.Win32.OpenFileDialog
+ var openFileDialog = new OpenFileDialog
{
Multiselect = true,
Filter = "All Files (*.*)|*.*"
@@ -352,7 +351,7 @@ namespace HeBianGu.App.Disk
// 构建配置
var config = builder.Build();
// 从配置获取MinIO设置更安全
- IMinioClient client = new Minio.MinioClient()
+ IMinioClient client = new MinioClient()
.WithEndpoint(config["Minio:Endpoint"])
.WithCredentials(config["Minio:AccessKey"], config["Minio:SecretKey"])
.Build();
@@ -426,7 +425,7 @@ namespace HeBianGu.App.Disk
{
Process.Start("shutdown", "/s /t 0");
}
- catch (System.ComponentModel.Win32Exception ex)
+ catch (Win32Exception ex)
{
MessageBox.Show($"关机失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
diff --git a/ViewModel/Send/DownViewModel.cs b/ViewModel/Send/DownViewModel.cs
index 94b2b2b..ff0d247 100644
--- a/ViewModel/Send/DownViewModel.cs
+++ b/ViewModel/Send/DownViewModel.cs
@@ -1,4 +1,10 @@
-using System.Collections.ObjectModel;
+using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Windows.Data;
+using System.Windows.Input;
using HeBianGu.Base.WpfBase;
using HeBianGu.Service.Mvc;
using Hopetry.Models;
@@ -13,10 +19,71 @@ namespace Hopetry.ViewModel.Send;
public class DownViewModel : MvcViewModelBase
{
private readonly MinioService _minioService;
-
+
+ // todo 完成取消下载及暂停下载逻辑
+ public RelayCommand OpenDownItemFolder { get; set; }
+ public RelayCommand LoadData { get; set; }
+ private readonly ConcurrentQueue _taskQueue = new();
+ private SemaphoreSlim _concurrencySemaphore;
+
+ private CancellationTokenSource _processingCts = new();
+
+ public string _AllTaskHeader;
+
+ public string AllTaskHeader
+ {
+ get => _AllTaskHeader;
+ set
+ {
+ _AllTaskHeader = value;
+ RaisePropertyChanged();
+ }
+ }
+
+ // 绑定属性
+ public ObservableCollection Tasks { get; } = new();
+
+ // 所有任务
+ public ObservableCollection AllTasks { get; set; }
+
+ public ICollectionView RunningTasksView { get; set; }
+ public ICollectionView PausedTasksView { get; set; }
+ private int _maxConcurrent = 3;
+
+ public int MaxConcurrent
+ {
+ get => _maxConcurrent;
+ set
+ {
+ if (value < 1) value = 1;
+ if (_maxConcurrent == value) return;
+
+ _maxConcurrent = value;
+ AdjustConcurrency();
+ RaisePropertyChanged();
+ }
+ }
+
+ private int _runningTasks;
+
+ public int RunningTasks
+ {
+ get => _runningTasks;
+ private set
+ {
+ _runningTasks = value;
+ RaisePropertyChanged();
+ }
+ }
+
+ // 命令
+ public ICommand AddTaskCommand { get; }
+ public ICommand ClearFinishedCommand { get; }
+
private ObservableCollection _downItems;
+
public ObservableCollection DownItems
{
get => _downItems;
@@ -32,9 +99,139 @@ public class DownViewModel : MvcViewModelBase
_minioService = minioService;
Console.WriteLine("初始化DownViewModel");
var client = SqlSugarConfig.GetSqlSugarScope();
- var data = client.Ado.SqlQuery($"select * from f_down_item");
- DownItems = new ObservableCollection(data);
+ var data = client.Ado.SqlQuery($"select * from download_task");
+ //DownItems = new ObservableCollection(data);
+ AllTasks = new ObservableCollection(data);
+ AllTaskHeader = $"全部({AllTasks.Count})";
Console.WriteLine(JsonConvert.SerializeObject(data));
+ OpenDownItemFolder = new RelayCommand(DoOpenDownItemFolder);
+ // 命令初始化
+ // AddTaskCommand = new RelayCommand(AddTask, CanAddTask);
+ // 修正:使用 CollectionViewSource 确保通知
+ var cvsRunning = new CollectionViewSource { Source = AllTasks };
+ cvsRunning.Filter += (s, e) =>
+ {
+ var b = ((MinioDownloadTask)e.Item).Status == "下载中";
+ e.Accepted = b;
+ };
+ RunningTasksView = cvsRunning.View;
+
+ var cvsPaused = new CollectionViewSource { Source = AllTasks };
+ cvsPaused.Filter += (s, e) =>
+ {
+ var b = ((MinioDownloadTask)e.Item).Status == "已完成";
+ e.Accepted = b;
+ };
+ PausedTasksView = cvsPaused.View;
+ // 关键:订阅数据源变更事件,强制视图刷新
+ AllTasks.CollectionChanged += (s, e) =>
+ {
+ RunningTasksView.Refresh();
+ PausedTasksView.Refresh();
+ };
+ Console.WriteLine($"运行中任务:{JsonConvert.SerializeObject(RunningTasksView)}");
+ Console.WriteLine($"已完成任务:{JsonConvert.SerializeObject(PausedTasksView)}");
+ ClearFinishedCommand = new CustomCommand(() =>
+ {
+ // todo 删除已完成的下载记录
+ });
+
+ // 启动任务处理线程
+ _concurrencySemaphore = new SemaphoreSlim(_maxConcurrent);
+ new Thread(ProcessTasksLoop) { IsBackground = true }.Start();
+ }
+
+ private void ProcessTasksLoop()
+ {
+ while (!_processingCts.IsCancellationRequested)
+ {
+ // 按当前并发数启动任务
+ for (int i = 0; i < _maxConcurrent; i++)
+ {
+ if (_taskQueue.TryDequeue(out var task))
+ {
+ _ = ExecuteTaskAsync(task);
+ }
+ }
+
+ Thread.Sleep(100);
+ }
+ }
+
+ private void AddTask()
+ {
+ var task = new MinioDownloadTask(_minioService, NewBucket, NewObjectName);
+ Tasks.Add(task);
+ _taskQueue.Enqueue(task);
+ RaisePropertyChanged(nameof(NewBucket));
+ RaisePropertyChanged(nameof(NewObjectName));
+ }
+
+ private bool CanAddTask() => !string.IsNullOrEmpty(NewBucket) && !string.IsNullOrEmpty(NewObjectName);
+
+ private async Task ExecuteTaskAsync(MinioDownloadTask task)
+ {
+ RunningTasks++;
+ try
+ {
+ // todo 配置下载路径
+ var savePath = Path.Combine(task.FileName);
+ await task.StartDownload(savePath);
+ }
+ finally
+ {
+ RunningTasks--;
+ // 任务完成后自动处理下一个
+ if (!_processingCts.IsCancellationRequested)
+ ProcessTasksLoop();
+ }
+ }
+
+ private void AdjustConcurrency()
+ {
+ lock (_concurrencySemaphore)
+ {
+ // 调整信号量容量
+ var delta = _maxConcurrent - _concurrencySemaphore.CurrentCount;
+ if (delta > 0)
+ {
+ for (int i = 0; i < delta; i++)
+ {
+ _concurrencySemaphore.Release();
+ }
+ }
+ }
+ }
+
+ // 绑定属性
+ private string _newBucket;
+
+ public string NewBucket
+ {
+ get => _newBucket;
+ set
+ {
+ _newBucket = value;
+ RaisePropertyChanged();
+ }
+ }
+
+ private string _newObjectName;
+
+ public string NewObjectName
+ {
+ get => _newObjectName;
+ set
+ {
+ _newObjectName = value;
+ RaisePropertyChanged();
+ }
+ }
+
+ public void DoOpenDownItemFolder(DownItem para)
+ {
+ Console.WriteLine($"点击了什么值:{JsonConvert.SerializeObject(para)}");
+ Process.Start("explorer.exe", para.FilePath);
}
protected override void Init()
@@ -44,7 +241,7 @@ public class DownViewModel : MvcViewModelBase
protected override void Loaded(string args)
{
}
-
+
[SugarTable("f_down_item")]
public class DownItem
{
@@ -52,32 +249,36 @@ public class DownViewModel : MvcViewModelBase
{
}
- [SugarColumn(IsPrimaryKey = true, IsIdentity = true,ColumnName = "id")]
+ [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnName = "id")]
public long Id { get; set; }
+
// 进度 已下载 多久下载完成 下载速度
- [SugarColumn(IsIgnore = true)]
- public int ProgressInt { get; set; }
+ [SugarColumn(IsIgnore = true)] public int ProgressInt { get; set; }
+
///
/// object key
///
[SugarColumn(ColumnName = "object_key")]
public string ObjectKey { get; set; }
+
///
/// 文件名称
///
[SugarColumn(ColumnName = "file_name")]
public string FileName { get; set; }
+
///
/// 文件类型
///
[SugarColumn(ColumnName = "file_type")]
public string FileType { get; set; }
+
///
/// 文件大小
///
[SugarColumn(ColumnName = "file_size")]
public long FileSize { get; set; }
-
+
///
/// 速度 100kb/s
///
@@ -89,12 +290,11 @@ public class DownViewModel : MvcViewModelBase
///
[SugarColumn(ColumnName = "file_path")]
public string FilePath { get; set; }
+
///
/// 文件的ETag
///
[SugarColumn(ColumnName = "file_etag")]
public string FileETag { get; set; }
-
-
}
}
\ No newline at end of file
diff --git a/ViewModel/Sync/SyncViewModel.cs b/ViewModel/Sync/SyncViewModel.cs
index 4a5a909..2dc4232 100644
--- a/ViewModel/Sync/SyncViewModel.cs
+++ b/ViewModel/Sync/SyncViewModel.cs
@@ -4,6 +4,7 @@ using System.Windows;
using System.Windows.Input;
using HeBianGu.Base.WpfBase;
using HeBianGu.Service.Mvc;
+using Hopetry.Models;
using Hopetry.Services;
using Microsoft.WindowsAPICodePack.Dialogs;
using SystemSetting = FileUploader.Models.SystemSetting;
@@ -17,6 +18,9 @@ public class SyncViewModel : MvcViewModelBase
private readonly ISerializerService _serializerService;
private readonly MinioService _minioService;
private readonly SystemSetting _setting;
+ public CustomCommand SyncData { get; set; }
+ public CustomCommand ComboBox_SelectionChanged { get; set; }
+ public CustomCommand OpenTargetDirCommand { get; set; }
protected override void Init()
{
@@ -39,10 +43,10 @@ public class SyncViewModel : MvcViewModelBase
_setting = new SystemSetting();
}
- ComboBox_SelectionChanged = new RelayCommand(async () => await ComboBox_SelectionUpdate());
- OpenDirCommand = new RelayCommand(async () => await ButtonBase_OnClick());
- OpenTargetDirCommand = new RelayCommand(async () => await Button_OpenTargetDir());
- SyncData = new RelayCommand(async () => await SyncDataQuick());
+ ComboBox_SelectionChanged = new CustomCommand(async () => await ComboBox_SelectionUpdate());
+ OpenDirCommand = new CustomCommand(async () => await ButtonBase_OnClick());
+ OpenTargetDirCommand = new CustomCommand(async () => await Button_OpenTargetDir());
+ SyncData = new CustomCommand(async () => await SyncDataQuick());
}
private async Task ComboBox_SelectionUpdate()
@@ -85,32 +89,9 @@ public class SyncViewModel : MvcViewModelBase
}
}
- public RelayCommand SyncData { get; set; }
- public RelayCommand ComboBox_SelectionChanged { get; set; }
- public RelayCommand OpenTargetDirCommand { get; set; }
- public class RelayCommand : ICommand
- {
- private readonly Action _execute;
-
- public RelayCommand(Action execute)
- {
- _execute = execute;
- }
-
- public bool CanExecute(object parameter)
- {
- return true;
- }
-
- public event EventHandler CanExecuteChanged;
-
- public void Execute(object parameter)
- {
- _execute();
- }
- }
+
private async Task Button_OpenTargetDir()
{
diff --git a/minio.db b/minio.db
index 687844bb6c2be0c9ddf8902c45faa7757bd43abe..f7af0ad94b9d5c285120be0719cd02c2a5bc7847 100644
GIT binary patch
literal 73728
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
S8IA=tL6{X12tu`>C-{E})Kd`v
delta 284
zcmZoTz|ydQae}lUD+2=q2*Uu=L>*&MRtCMYPG0^W3@kiL8TdBypXK?^vvjkdfFaN1
zr943_&6dpUn}73cV4U2?!_KI=c{87%qBPI|F5WZ-{vG@U{6>5q`L^)o@Tv1Y;$6*~
z2Gr@r%i_$%!7w??)<`lhJ+&k;JwDAa)hNX{*~FBSgF#tQm|0wXa=V=_mooivn)<>lvg(r^?X0=n*nnJPVHSDy$r`qHEY_?*Gk4gP@+X-mo2A$=
zb1(xiJCNT8c7Ds!y)6w_|Z*1ghO*@4?2%|DJ*W{boUhd;Ajv*ai6+
TnUxvS;#2a=^Wrm0QggWg