Skip to content

Commit 2e7837d

Browse files
committed
feat(*.cs & *.xaml): Add video recording and playback for debts
Introduces camera/video recording support using Emgu.CV and ReactiveUI, enabling borrowers to record and upload collateral videos for debts. Adds new services (CameraService, ICameraService), view models (VideoRecorderViewModel), and views (VideoRecorderWindow, VideoPlayerWindow, KeyEntryWindow) for video capture and playback. DebtModel and DebtDto are updated to support video metadata. DebtViewModel now supports video upload, marking debts as defaulted, and secure video viewing for lenders. Filtering and charting improvements are added to Account, Budget, and Transactions view models. Updates dependencies in the project file to support new features.
1 parent dc84d82 commit 2e7837d

27 files changed

+1478
-319
lines changed

FinTrack/App.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using FinTrackForWindows.Services.Accounts;
55
using FinTrackForWindows.Services.Api;
66
using FinTrackForWindows.Services.Budgets;
7+
using FinTrackForWindows.Services.Camera;
78
using FinTrackForWindows.Services.Currencies;
89
using FinTrackForWindows.Services.Debts;
910
using FinTrackForWindows.Services.Memberships;
@@ -87,6 +88,8 @@ private void ConfigureServices(IServiceCollection services)
8788
services.AddSingleton<ICurrenciesStore, CurrenciesStore>();
8889
services.AddSingleton<IMembershipStore, MembershipStore>();
8990
services.AddSingleton<IDebtStore, DebtStore>();
91+
92+
services.AddTransient<ICameraService, CameraService>();
9093
}
9194

9295
protected override async void OnStartup(StartupEventArgs e)

FinTrack/Dtos/DebtDtos/DebtDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class DebtDto
1818
public DateTime DueDateUtc { get; set; }
1919
public string Description { get; set; } = null!;
2020
public DebtStatusType Status { get; set; }
21+
public int? VideoMetadataId { get; set; }
2122
public DateTime CreateAtUtc { get; set; }
2223
public DateTime? UpdatedAtUtc { get; set; }
2324
public DateTime? PaidAtUtc { get; set; }

FinTrack/Enums/DebtStatus.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
public enum DebtStatusType
44
{
55
PendingBorrowerAcceptance, // Borç Alan Onayı Bekliyor
6-
AcceptedPendingVideoUpload, // Borçlu Kabul Etti, Video Yüklemesi Bekleniyor (YENİ)
6+
AcceptedPendingVideoUpload, // Borçlu Kabul Etti, Video Yüklemesi Bekleniyor
77
PendingOperatorApproval, // Operatör Onayı Bekliyor
88
Active, // Aktif Borç
99
PaymentConfirmationPending, // Ödeme Onayı Bekliyor

FinTrack/FinTrackForWindows.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343

4444
<ItemGroup>
4545
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
46+
<PackageReference Include="Emgu.CV.Bitmap" Version="4.11.0.5746" />
47+
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.11.0.5746" />
4648
<PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc5.4" />
4749
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
4850
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0-preview.6.25358.103">
@@ -56,10 +58,12 @@
5658
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
5759
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
5860
<PackageReference Include="Npgsql" Version="9.0.3" />
61+
<PackageReference Include="ReactiveUI.WPF" Version="20.4.1" />
5962
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.1-dev-02307" />
6063
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.1-dev-02317" />
6164
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
6265
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
66+
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
6367
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
6468
</ItemGroup>
6569

FinTrack/Models/Debt/DebtModel.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public partial class DebtModel : ObservableObject
1111
public int BorrowerId { get; set; }
1212
public int CurrentUserId { get; set; }
1313

14+
public int? VideoMetadataId { get; set; }
15+
1416
[ObservableProperty]
1517
private string lenderName = string.Empty;
1618

@@ -21,14 +23,20 @@ public partial class DebtModel : ObservableObject
2123
private decimal amount;
2224

2325
[ObservableProperty]
26+
[NotifyPropertyChangedFor(nameof(CanMarkAsDefaulted))]
2427
private DateTime dueDate;
2528

2629
public string borrowerImageUrl = string.Empty;
2730

2831
public string lenderImageUrl = string.Empty;
2932

3033
[ObservableProperty]
31-
[NotifyPropertyChangedFor(nameof(StatusText), nameof(StatusBrush), nameof(IsActionRequiredForBorrower), nameof(IsRejected))]
34+
[NotifyPropertyChangedFor(nameof(StatusText))]
35+
[NotifyPropertyChangedFor(nameof(StatusBrush))]
36+
[NotifyPropertyChangedFor(nameof(IsActionRequiredForBorrower))]
37+
[NotifyPropertyChangedFor(nameof(IsRejected))]
38+
[NotifyPropertyChangedFor(nameof(IsVideoViewableForLender))]
39+
[NotifyPropertyChangedFor(nameof(CanMarkAsDefaulted))]
3240
private DebtStatusType status;
3341

3442
public bool IsCurrentUserTheBorrower => BorrowerId == CurrentUserId;
@@ -44,13 +52,14 @@ public partial class DebtModel : ObservableObject
4452

4553
public Brush StatusBrush => Status switch
4654
{
47-
DebtStatusType.Active => new SolidColorBrush(Colors.Green),
48-
DebtStatusType.Defaulted => new SolidColorBrush(Colors.DarkRed),
49-
DebtStatusType.AcceptedPendingVideoUpload => new SolidColorBrush(Colors.CornflowerBlue),
50-
DebtStatusType.PendingBorrowerAcceptance => new SolidColorBrush(Colors.DodgerBlue),
51-
DebtStatusType.PendingOperatorApproval => new SolidColorBrush(Colors.Orange),
52-
DebtStatusType.RejectedByOperator => new SolidColorBrush(Colors.Red),
53-
DebtStatusType.RejectedByBorrower => new SolidColorBrush(Colors.Red),
55+
DebtStatusType.Active => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")), // Green
56+
DebtStatusType.Defaulted => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B71C1C")), // Dark Red
57+
DebtStatusType.AcceptedPendingVideoUpload => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1E88E5")), // Blue
58+
DebtStatusType.PendingBorrowerAcceptance => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#03A9F4")), // Light Blue
59+
DebtStatusType.PendingOperatorApproval => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FB8C00")), // Orange
60+
DebtStatusType.RejectedByOperator => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E53935")), // Red
61+
DebtStatusType.RejectedByBorrower => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E53935")), // Red
62+
DebtStatusType.Paid => new SolidColorBrush(Colors.Gray),
5463
_ => new SolidColorBrush(Colors.Gray)
5564
};
5665

@@ -61,17 +70,32 @@ public partial class DebtModel : ObservableObject
6170
DebtStatusType.PendingOperatorApproval => "Operatör Onayı Bekleniyor",
6271
DebtStatusType.Active => "Aktif",
6372
DebtStatusType.Paid => "Ödendi",
64-
DebtStatusType.Defaulted => "Vadesi Geçmiş",
73+
DebtStatusType.Defaulted => "Vadesi Geçmiş - Temerrüt",
6574
DebtStatusType.RejectedByBorrower => "Tarafınızdan Reddedildi",
6675
DebtStatusType.RejectedByOperator => "Operatör Tarafından Reddedildi",
6776
_ => "Bilinmeyen Durum"
6877
};
6978

70-
// Borçlunun video yüklemesi gerekip gerekmediğini kontrol eder.
7179
public bool IsActionRequiredForBorrower =>
7280
Status == DebtStatusType.AcceptedPendingVideoUpload && IsCurrentUserTheBorrower;
7381

7482
public bool IsRejected =>
7583
Status == DebtStatusType.RejectedByBorrower || Status == DebtStatusType.RejectedByOperator;
84+
85+
/// <summary>
86+
/// "Teminat Videosunu İzle" düğmesinin görünür olup olmayacağını belirler.
87+
/// </summary>
88+
public bool IsVideoViewableForLender =>
89+
Status == DebtStatusType.Defaulted && // Durum 'Defaulted' olmalı
90+
IsCurrentUserTheLender && // Kullanıcı borç veren olmalı
91+
VideoMetadataId.HasValue; // İlişkili bir video olmalı
92+
93+
/// <summary>
94+
/// "Temerrüde Düştü Olarak İşaretle" düğmesinin görünür olup olmayacağını belirleyecek.
95+
/// </summary>
96+
public bool CanMarkAsDefaulted =>
97+
Status == DebtStatusType.Active && // Durum 'Active' olmalı
98+
IsCurrentUserTheLender && // Kullanıcı borç veren olmalı
99+
DueDate < DateTime.Now; // Vadesi geçmiş olmalı
76100
}
77101
}

FinTrack/Services/Api/ApiService.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public ApiService(ILogger<ApiService> logger, IConfiguration configuration)
2525
_logger = logger;
2626
_configuration = configuration;
2727

28-
_baseUrl = "http://localhost:5000/";
28+
_baseUrl = "http://localhost:8090/";
2929
//_baseUrl = _configuration["BaseServerUrl"];
3030
_httpClient = new HttpClient
3131
{
@@ -381,5 +381,36 @@ public async Task<bool> UploadFileAsync(string endpoint, string filePath)
381381
throw;
382382
}
383383
}
384+
385+
public async Task<(Stream? Stream, string? ContentType, string? FileName)> StreamFileAsync(string endpoint)
386+
{
387+
_logger.LogInformation("Streaming file request started: {Endpoint}", endpoint);
388+
try
389+
{
390+
AddAuthorizationHeader();
391+
392+
var response = await _httpClient.GetAsync(endpoint, HttpCompletionOption.ResponseHeadersRead);
393+
394+
response.EnsureSuccessStatusCode();
395+
396+
var stream = await response.Content.ReadAsStreamAsync();
397+
398+
var contentType = response.Content.Headers.ContentType?.ToString();
399+
var fileName = response.Content.Headers.ContentDisposition?.FileName?.Trim('"');
400+
401+
_logger.LogInformation("File stream successfully retrieved from {Endpoint}", endpoint);
402+
return (stream, contentType, fileName);
403+
}
404+
catch (HttpRequestException ex)
405+
{
406+
_logger.LogError(ex, "HTTP error during file stream: {Endpoint}. Status: {StatusCode}", endpoint, ex.StatusCode);
407+
return (null, null, null);
408+
}
409+
catch (Exception ex)
410+
{
411+
_logger.LogError(ex, "Generic error during file stream: {Endpoint}", endpoint);
412+
return (null, null, null);
413+
}
414+
}
384415
}
385416
}

FinTrack/Services/Api/IApiService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace FinTrackForWindows.Services.Api
1+
using System.IO;
2+
3+
namespace FinTrackForWindows.Services.Api
24
{
35
public interface IApiService
46
{
@@ -9,5 +11,6 @@ public interface IApiService
911
Task<bool> CreateCategoryAsync(string endpoint, object payload);
1012
Task<bool> UploadFileAsync(string endpoint, string filePath);
1113
Task<(byte[] FileBytes, string FileName)?> PostAndDownloadReportAsync<T>(string endpoint, T payload);
14+
Task<(Stream? Stream, string? ContentType, string? FileName)> StreamFileAsync(string endpoint);
1215
}
1316
}

FinTrack/Services/AuthService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public AuthService(IConfiguration configuration)
1616
_configuration = configuration;
1717
_httpClient = new HttpClient
1818
{
19-
BaseAddress = new Uri("http://localhost:5000/")
19+
BaseAddress = new Uri("http://localhost:8090/")
2020
//BaseAddress = new Uri(_configuration["BaseServerUrl"])
2121
};
2222

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using Emgu.CV;
2+
using Emgu.CV.CvEnum;
3+
using Microsoft.Extensions.Logging;
4+
using System.Drawing.Imaging;
5+
using System.IO;
6+
using System.Windows.Media.Imaging;
7+
using System.Windows.Threading;
8+
9+
namespace FinTrackForWindows.Services.Camera
10+
{
11+
public class CameraService : ICameraService
12+
{
13+
private VideoCapture? _capture;
14+
private VideoWriter? _writer;
15+
private DispatcherTimer? _timer;
16+
17+
private readonly ILogger<CameraService> _logger;
18+
19+
public Action<BitmapSource>? OnFrameReady { get; set; }
20+
21+
public CameraService(ILogger<CameraService> logger)
22+
{
23+
_logger = logger;
24+
}
25+
26+
public bool InitializeCamera(int cameraIndex = 0)
27+
{
28+
try
29+
{
30+
_capture = new VideoCapture(cameraIndex);
31+
if (!_capture.IsOpened) return false;
32+
33+
double fps = _capture.Get(CapProp.Fps);
34+
_timer = new DispatcherTimer
35+
{
36+
Interval = TimeSpan.FromMilliseconds(1000 / (fps > 0 ? fps : 30))
37+
};
38+
_timer.Tick += Timer_Tick;
39+
_timer.Start();
40+
return true;
41+
}
42+
catch(Exception ex)
43+
{
44+
_logger.LogError(ex, "Failed to initialize camera.");
45+
return false;
46+
}
47+
}
48+
49+
private void Timer_Tick(object? sender, EventArgs e)
50+
{
51+
if (_capture == null || !_capture.IsOpened) return;
52+
53+
using (Mat frame = _capture.QueryFrame())
54+
{
55+
if (frame != null)
56+
{
57+
OnFrameReady?.Invoke(ConvertMatToBitmapSource(frame));
58+
59+
if (_writer != null && _writer.IsOpened)
60+
{
61+
_writer.Write(frame);
62+
}
63+
}
64+
}
65+
}
66+
67+
public string StartRecording()
68+
{
69+
if (_capture == null || !_capture.IsOpened)
70+
throw new InvalidOperationException("Camera is not initialized.");
71+
72+
string tempPath = Path.GetTempPath();
73+
string outputFilePath = Path.Combine(tempPath, $"debt_video_{Guid.NewGuid()}.mp4");
74+
75+
int frameWidth = (int)_capture.Get(CapProp.FrameWidth);
76+
int frameHeight = (int)_capture.Get(CapProp.FrameHeight);
77+
double fps = _capture.Get(CapProp.Fps);
78+
79+
_writer = new VideoWriter(outputFilePath, VideoWriter.Fourcc('X', '2', '6', '4'), fps > 0 ? fps : 30, new System.Drawing.Size(frameWidth, frameHeight), true);
80+
return outputFilePath;
81+
}
82+
83+
public void StopRecording()
84+
{
85+
_writer?.Dispose();
86+
_writer = null;
87+
}
88+
89+
public void Release()
90+
{
91+
_timer?.Stop();
92+
_capture?.Dispose();
93+
_writer?.Dispose();
94+
}
95+
96+
public void Dispose()
97+
{
98+
Release();
99+
GC.SuppressFinalize(this);
100+
}
101+
102+
private static BitmapSource ConvertMatToBitmapSource(Mat image)
103+
{
104+
using var bitmap = image.ToBitmap();
105+
using var stream = new MemoryStream();
106+
bitmap.Save(stream, ImageFormat.Bmp);
107+
stream.Position = 0;
108+
var bitmapImage = new BitmapImage();
109+
bitmapImage.BeginInit();
110+
bitmapImage.StreamSource = stream;
111+
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
112+
bitmapImage.EndInit();
113+
bitmapImage.Freeze();
114+
return bitmapImage;
115+
}
116+
}
117+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Windows.Media.Imaging;
2+
3+
namespace FinTrackForWindows.Services.Camera
4+
{
5+
public interface ICameraService : IDisposable
6+
{
7+
Action<BitmapSource> OnFrameReady { get; set; }
8+
bool InitializeCamera(int cameraIndex = 0);
9+
string StartRecording();
10+
void StopRecording();
11+
void Release();
12+
}
13+
}

0 commit comments

Comments
 (0)