Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6efa72745 | |||
| 2ce47ee305 | |||
| 90b2abd587 |
@@ -3,7 +3,11 @@
|
||||
"allow": [
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")",
|
||||
"Bash(dotnet build:*)"
|
||||
"Bash(dotnet build:*)",
|
||||
"WebFetch(domain:git.nouredeen.dev)",
|
||||
"WebFetch(domain:supabase.com)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cmd:*)"
|
||||
]
|
||||
},
|
||||
"spinnerTipsEnabled": true
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Build Linux
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
dotnet publish Clario.Desktop/Clario.Desktop.csproj \
|
||||
-r linux-x64 \
|
||||
-c Release \
|
||||
--self-contained true \
|
||||
-p:PublishSingleFile=false \
|
||||
-o ./publish/linux-x64
|
||||
|
||||
|
||||
- name: Package as tar.gz
|
||||
run: tar -czf Clario-linux-x64.tar.gz -C ./publish/linux-x64 .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: Clario-linux-x64
|
||||
path: ./Clario-linux-x64.tar.gz
|
||||
|
||||
retention-days: 7
|
||||
40
.github/workflows/build-linux.yml
vendored
40
.github/workflows/build-linux.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Build Linux
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
dotnet publish Clario.Desktop/Clario.Desktop.csproj \
|
||||
-r linux-x64 \
|
||||
-c Release \
|
||||
--self-contained true \
|
||||
-p:PublishSingleFile=true \
|
||||
-o ./publish/linux-x64
|
||||
|
||||
|
||||
- name: Package as tar.gz
|
||||
run: tar -czf Clario-linux-x64.tar.gz -C ./publish/linux-x64 .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Clario-linux-x64
|
||||
path: ./publish/linux
|
||||
retention-days: 7
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,4 +7,7 @@ obj/
|
||||
./Clario/CLAUDE_CONTEXT.md
|
||||
publish/
|
||||
*.tar.gz
|
||||
Clario/devsettings.json
|
||||
Clario/devsettings.json
|
||||
.env
|
||||
TODO.md
|
||||
clario.keystore
|
||||
@@ -1,21 +1,50 @@
|
||||
using Android.App;
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Avalonia;
|
||||
using Avalonia.Android;
|
||||
using Clario;
|
||||
|
||||
namespace Clario.Android;
|
||||
|
||||
[Activity(
|
||||
Label = "Clario.Android",
|
||||
Label = "Clario",
|
||||
Theme = "@style/MyTheme.NoActionBar",
|
||||
Icon = "@drawable/icon",
|
||||
MainLauncher = true,
|
||||
LaunchMode = LaunchMode.SingleTop,
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
|
||||
[IntentFilter(
|
||||
new[] { Intent.ActionView },
|
||||
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
|
||||
DataScheme = "clario",
|
||||
DataHost = "auth")]
|
||||
public class MainActivity : AvaloniaMainActivity<App>
|
||||
{
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
// Capture deep link before Avalonia initializes
|
||||
var uri = Intent?.DataString;
|
||||
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
|
||||
App.PendingDeepLink = uri;
|
||||
|
||||
base.OnCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent? intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
// Called when app is already running (SingleTop) and link is opened again
|
||||
var uri = intent?.DataString;
|
||||
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
|
||||
_ = App.HandleDeepLink(uri);
|
||||
}
|
||||
|
||||
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
|
||||
{
|
||||
return base.CustomizeAppBuilder(builder)
|
||||
.WithInterFont();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"GeneralSettings": {
|
||||
"NetProjectPath": "Clario.Desktop.csproj",
|
||||
"ApplicationName": "Clario",
|
||||
"Version": "0.4.0",
|
||||
"Version": "0.6.0",
|
||||
"PackageName": {
|
||||
"$type": "msbuild",
|
||||
"property": "AssemblyName"
|
||||
@@ -13,11 +13,11 @@
|
||||
}
|
||||
},
|
||||
"LinuxSettings": {
|
||||
"AppIcon": "../Clario/Assets/Logo.png",
|
||||
"AppIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
|
||||
"CreateBinSymlink": "True"
|
||||
},
|
||||
"Win32Settings": {
|
||||
"InstallerIcon": "../Clario/Assets/Clario-Logo.svg",
|
||||
"InstallerIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
|
||||
"Company": "Clario",
|
||||
"IncludeUninstaller": "True"
|
||||
},
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Clario;
|
||||
|
||||
namespace Clario.Desktop;
|
||||
|
||||
sealed class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
||||
BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
// Capture deep link passed as command-line arg by Windows protocol handler
|
||||
var deepLink = args.FirstOrDefault(a =>
|
||||
a.StartsWith("clario://", StringComparison.OrdinalIgnoreCase));
|
||||
if (deepLink != null)
|
||||
App.PendingDeepLink = deepLink;
|
||||
|
||||
// Register clario:// URL scheme on Windows (idempotent)
|
||||
RegisterUrlScheme();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
@@ -22,4 +28,23 @@ sealed class Program
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
|
||||
private static void RegisterUrlScheme()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
try
|
||||
{
|
||||
var exe = Environment.ProcessPath
|
||||
?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (exe is null) return;
|
||||
|
||||
using var key = Microsoft.Win32.Registry.CurrentUser
|
||||
.CreateSubKey(@"SOFTWARE\Classes\clario");
|
||||
key.SetValue("", "URL:Clario Protocol");
|
||||
key.SetValue("URL Protocol", "");
|
||||
using var cmd = key.CreateSubKey(@"shell\open\command");
|
||||
cmd.SetValue("", $"\"{exe}\" \"%1\"");
|
||||
}
|
||||
catch { /* ignore — no registry write access in sandboxed environments */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +36,15 @@
|
||||
<StyleInclude Source="../Theme/AppTheme.axaml" />
|
||||
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
|
||||
<StyleInclude Source="avares://FluentAvalonia.ProgressRing/Styling/Controls/ProgressRing.axaml" />
|
||||
<!-- Must come after ColorPicker Fluent.xaml to override Width="64" setter -->
|
||||
<Styles>
|
||||
<Style Selector="ColorPicker">
|
||||
<Setter Property="Width" Value="NaN" />
|
||||
</Style>
|
||||
<Style Selector="ColorPicker /template/ DropDownButton">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
@@ -17,6 +18,47 @@ public partial class App : Application
|
||||
{
|
||||
public static bool IsMobile { get; private set; }
|
||||
|
||||
/// <summary>Set before OnFrameworkInitializationCompleted runs (from Program.cs or MainActivity).</summary>
|
||||
public static string? PendingDeepLink { get; set; }
|
||||
|
||||
/// <summary>Called from MainActivity.OnNewIntent when app is already running.</summary>
|
||||
public static async Task HandleDeepLink(string deepLink)
|
||||
{
|
||||
var (accessToken, refreshToken, type) = ParseDeepLinkFragment(deepLink);
|
||||
if (type != "recovery" || accessToken is null) return;
|
||||
|
||||
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
|
||||
|
||||
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var vm = new ResetPasswordViewModel();
|
||||
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
desktop.MainWindow!.DataContext = vm;
|
||||
else if (Current?.ApplicationLifetime is ISingleViewApplicationLifetime sv)
|
||||
sv.MainView!.DataContext = vm;
|
||||
});
|
||||
}
|
||||
|
||||
private static (string? accessToken, string? refreshToken, string? type) ParseDeepLinkFragment(string url)
|
||||
{
|
||||
var hash = url.IndexOf('#');
|
||||
if (hash < 0) return default;
|
||||
string? at = null, rt = null, type = null;
|
||||
foreach (var part in url[(hash + 1)..].Split('&'))
|
||||
{
|
||||
var eq = part.IndexOf('=');
|
||||
if (eq < 0) continue;
|
||||
var val = Uri.UnescapeDataString(part[(eq + 1)..]);
|
||||
switch (part[..eq])
|
||||
{
|
||||
case "access_token": at = val; break;
|
||||
case "refresh_token": rt = val; break;
|
||||
case "type": type = val; break;
|
||||
}
|
||||
}
|
||||
return (at, rt, type);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
@@ -73,19 +115,32 @@ public partial class App : Application
|
||||
ThemeService.SwitchToTheme(profile.Theme);
|
||||
}
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
// Check for deep link from password reset email
|
||||
ViewModelBase targetViewModel;
|
||||
if (PendingDeepLink is { } deepLink && deepLink.Contains("type=recovery"))
|
||||
{
|
||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
var (accessToken, refreshToken, _) = ParseDeepLinkFragment(deepLink);
|
||||
if (accessToken is not null)
|
||||
{
|
||||
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
|
||||
}
|
||||
PendingDeepLink = null;
|
||||
targetViewModel = new ResetPasswordViewModel();
|
||||
}
|
||||
else
|
||||
{
|
||||
targetViewModel = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
}
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
desktop.MainWindow!.DataContext = targetViewModel;
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||
{
|
||||
DebugLogger.Log("ANDROID PATH HIT");
|
||||
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
singleViewPlatform.MainView!.DataContext = targetViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
<Compile Update="Views\AnalyticsView.axaml.cs">
|
||||
<DependentUpon>AnalyticsView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\DashboardSkeletonView.axaml.cs">
|
||||
<DependentUpon>DashboardSkeletonView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
@@ -57,4 +60,11 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
|
||||
<AndroidKeyStore>true</AndroidKeyStore>
|
||||
<AndroidSigningKeyStore>clario.keystore</AndroidSigningKeyStore>
|
||||
<AndroidSigningKeyAlias>clario</AndroidSigningKeyAlias>
|
||||
<AndroidSigningKeyPass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningKeyPass>
|
||||
<AndroidSigningStorePass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningStorePass>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
the internal template parts at ControlTheme priority, making
|
||||
external /template/ style selectors unreliable for CalendarButton.
|
||||
Replacing the entire ControlTheme is the only reliable approach.
|
||||
-->
|
||||
-->
|
||||
<ControlTheme x:Key="{x:Type CalendarButton}" TargetType="CalendarButton">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
@@ -69,7 +69,7 @@
|
||||
</Styles.Resources>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- DateRangePicker control template -->
|
||||
<!-- DateRangePicker control template -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<Style Selector="local|DateRangePicker">
|
||||
@@ -86,6 +86,7 @@
|
||||
<Grid>
|
||||
<Button x:Name="PART_Button"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
@@ -147,7 +148,7 @@
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- CalendarItem: nav header buttons (prev / title / next) -->
|
||||
<!-- CalendarItem: nav header buttons (prev / title / next) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton">
|
||||
|
||||
@@ -25,7 +25,6 @@ public class DateRangePicker : TemplatedControl
|
||||
set => SetValue(SelectionModeProperty, value);
|
||||
}
|
||||
|
||||
// FIX: Use DirectProperty to avoid shared-instance default and get proper TwoWay support
|
||||
private IList<DateTime> _selectedDates = new List<DateTime>();
|
||||
|
||||
public static readonly DirectProperty<DateRangePicker, IList<DateTime>> SelectedDatesProperty =
|
||||
@@ -41,7 +40,6 @@ public class DateRangePicker : TemplatedControl
|
||||
set => SetAndRaise(SelectedDatesProperty, ref _selectedDates, value);
|
||||
}
|
||||
|
||||
// FIX: Add defaultBindingMode: TwoWay so changes propagate back to the ViewModel
|
||||
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
|
||||
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
|
||||
nameof(SelectedDate),
|
||||
@@ -116,7 +114,6 @@ public class DateRangePicker : TemplatedControl
|
||||
if (_isSyncing) return;
|
||||
if (_popup is null || !_popup.IsOpen) return;
|
||||
|
||||
// FIX: Ignore clicks on the nav buttons/header — only react to day cell clicks
|
||||
if (e.Source is not Control source) return;
|
||||
if (source.TemplatedParent is CalendarDayButton == false &&
|
||||
source.FindAncestorOfType<CalendarDayButton>() is null)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using Clario.Models;
|
||||
using Clario.Models.GeneralModels;
|
||||
using Clario.Services;
|
||||
@@ -15,6 +16,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Clario.Messages;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Supabase.Postgrest;
|
||||
using Supabase.Realtime.PostgresChanges;
|
||||
using Constants = Supabase.Realtime.Constants;
|
||||
using FileOptions = Supabase.Storage.FileOptions;
|
||||
|
||||
namespace Clario.Data;
|
||||
@@ -174,12 +177,14 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
LinkTransactionAccounts(enriched);
|
||||
Transactions.Add(enriched);
|
||||
}
|
||||
|
||||
if (inResult.Models.Count >= 1)
|
||||
{
|
||||
var enriched = LinkTransactionCategories(inResult.Models[0]);
|
||||
LinkTransactionAccounts(enriched);
|
||||
Transactions.Add(enriched);
|
||||
}
|
||||
|
||||
// Re-enrich both so AccountDisplayText can reference the counterpart (from/to)
|
||||
LinkTransactionAccounts();
|
||||
}
|
||||
@@ -215,6 +220,7 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
Transactions[index] = enriched;
|
||||
}
|
||||
}
|
||||
|
||||
LinkTransactionAccounts();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -306,7 +312,7 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
|
||||
var accounts = await SupabaseService.Client.From<Account>().Get();
|
||||
Accounts = new ObservableCollection<Account>(accounts.Models);
|
||||
return accounts.Models.OrderBy(x=>x.IsPrimary).ThenBy(x=>x.CreatedAt).ToList();
|
||||
return accounts.Models.OrderBy(x => x.IsPrimary).ThenBy(x => x.CreatedAt).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
|
||||
@@ -719,4 +725,186 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
if (avatarUrl.StartsWith("http")) return avatarUrl;
|
||||
return $"{PublicBaseUrl}/{avatarUrl}";
|
||||
}
|
||||
|
||||
public void StartRealtimeSync()
|
||||
{
|
||||
if (SupabaseService.Client.Auth.CurrentUser?.Id is null) return;
|
||||
DebugLogger.Log("[Realtime] StartRealtimeSync: registering listeners");
|
||||
|
||||
// Transactions
|
||||
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
|
||||
{
|
||||
var insertedTransaction = c.Model<Transaction>();
|
||||
if (insertedTransaction is null) { DebugLogger.Log("[Realtime] Transaction INSERT: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Transaction INSERT: {insertedTransaction.Id} ({insertedTransaction.Description})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (Transactions.Any(x => x.Id == insertedTransaction.Id)) { DebugLogger.Log($"[Realtime] Transaction INSERT: skipped duplicate {insertedTransaction.Id}"); return; }
|
||||
LinkTransactionCategories(insertedTransaction);
|
||||
LinkTransactionAccounts(insertedTransaction);
|
||||
Transactions.Add(insertedTransaction);
|
||||
DebugLogger.Log($"[Realtime] Transaction INSERT: added to collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
|
||||
{
|
||||
var updatedTransaction = c.Model<Transaction>();
|
||||
if (updatedTransaction is null) { DebugLogger.Log("[Realtime] Transaction UPDATE: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Transaction UPDATE: {updatedTransaction.Id} ({updatedTransaction.Description})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var idx = Transactions.ToList().FindIndex(x => x.Id == updatedTransaction.Id);
|
||||
if (idx == -1) { DebugLogger.Log($"[Realtime] Transaction UPDATE: id {updatedTransaction.Id} not found in collection"); return; }
|
||||
LinkTransactionCategories(updatedTransaction);
|
||||
LinkTransactionAccounts(updatedTransaction);
|
||||
Transactions[idx] = updatedTransaction;
|
||||
DebugLogger.Log($"[Realtime] Transaction UPDATE: replaced at index {idx}");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
|
||||
{
|
||||
var deletedTransaction = c.OldModel<Transaction>();
|
||||
if (deletedTransaction is null) { DebugLogger.Log("[Realtime] Transaction DELETE: old model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Transaction DELETE: {deletedTransaction.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var item = Transactions.FirstOrDefault(x => x.Id == deletedTransaction.Id);
|
||||
if (item is not null) { Transactions.Remove(item); DebugLogger.Log($"[Realtime] Transaction DELETE: removed {deletedTransaction.Id}"); }
|
||||
else DebugLogger.Log($"[Realtime] Transaction DELETE: id {deletedTransaction.Id} not found (already removed locally)");
|
||||
});
|
||||
});
|
||||
|
||||
// Accounts
|
||||
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
|
||||
{
|
||||
var insertedAccount = c.Model<Account>();
|
||||
if (insertedAccount is null) { DebugLogger.Log("[Realtime] Account INSERT: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Account INSERT: {insertedAccount.Id} ({insertedAccount.Name})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (Accounts.Any(x => x.Id == insertedAccount.Id)) { DebugLogger.Log($"[Realtime] Account INSERT: skipped duplicate {insertedAccount.Id}"); return; }
|
||||
Accounts.Add(insertedAccount);
|
||||
DebugLogger.Log($"[Realtime] Account INSERT: added to collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
|
||||
{
|
||||
var updatedAccount = c.Model<Account>();
|
||||
if (updatedAccount is null) { DebugLogger.Log("[Realtime] Account UPDATE: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Account UPDATE: {updatedAccount.Id} ({updatedAccount.Name})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var idx = Accounts.ToList().FindIndex(x => x.Id == updatedAccount.Id);
|
||||
if (idx != -1) { Accounts[idx] = updatedAccount; DebugLogger.Log($"[Realtime] Account UPDATE: replaced at index {idx}"); }
|
||||
else DebugLogger.Log($"[Realtime] Account UPDATE: id {updatedAccount.Id} not found in collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
|
||||
{
|
||||
var deletedAccount = c.OldModel<Account>();
|
||||
if (deletedAccount is null) { DebugLogger.Log("[Realtime] Account DELETE: old model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Account DELETE: {deletedAccount.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var item = Accounts.FirstOrDefault(x => x.Id == deletedAccount.Id);
|
||||
if (item is not null) { Accounts.Remove(item); DebugLogger.Log($"[Realtime] Account DELETE: removed {deletedAccount.Id}"); }
|
||||
else DebugLogger.Log($"[Realtime] Account DELETE: id {deletedAccount.Id} not found (already removed locally)");
|
||||
});
|
||||
});
|
||||
|
||||
// Budgets
|
||||
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
|
||||
{
|
||||
var insertedBudget = c.Model<Budget>();
|
||||
if (insertedBudget is null) { DebugLogger.Log("[Realtime] Budget INSERT: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Budget INSERT: {insertedBudget.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (Budgets.Any(x => x.Id == insertedBudget.Id)) { DebugLogger.Log($"[Realtime] Budget INSERT: skipped duplicate {insertedBudget.Id}"); return; }
|
||||
Budgets.Add(insertedBudget);
|
||||
DebugLogger.Log($"[Realtime] Budget INSERT: added to collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
|
||||
{
|
||||
var updatedBudget = c.Model<Budget>();
|
||||
if (updatedBudget is null) { DebugLogger.Log("[Realtime] Budget UPDATE: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Budget UPDATE: {updatedBudget.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var idx = Budgets.ToList().FindIndex(x => x.Id == updatedBudget.Id);
|
||||
if (idx != -1) { Budgets[idx] = updatedBudget; DebugLogger.Log($"[Realtime] Budget UPDATE: replaced at index {idx}"); }
|
||||
else DebugLogger.Log($"[Realtime] Budget UPDATE: id {updatedBudget.Id} not found in collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
|
||||
{
|
||||
var deletedBudget = c.OldModel<Budget>();
|
||||
if (deletedBudget is null) { DebugLogger.Log("[Realtime] Budget DELETE: old model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Budget DELETE: {deletedBudget.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var item = Budgets.FirstOrDefault(x => x.Id == deletedBudget.Id);
|
||||
if (item is not null) { Budgets.Remove(item); DebugLogger.Log($"[Realtime] Budget DELETE: removed {deletedBudget.Id}"); }
|
||||
else DebugLogger.Log($"[Realtime] Budget DELETE: id {deletedBudget.Id} not found (already removed locally)");
|
||||
});
|
||||
});
|
||||
|
||||
// Categories
|
||||
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
|
||||
{
|
||||
var insertedCategory = c.Model<Category>();
|
||||
if (insertedCategory is null) { DebugLogger.Log("[Realtime] Category INSERT: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Category INSERT: {insertedCategory.Id} ({insertedCategory.Name})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (Categories.Any(x => x.Id == insertedCategory.Id)) { DebugLogger.Log($"[Realtime] Category INSERT: skipped duplicate {insertedCategory.Id}"); return; }
|
||||
Categories.Add(insertedCategory);
|
||||
DebugLogger.Log($"[Realtime] Category INSERT: added to collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
|
||||
{
|
||||
var UpdatedCategory = c.Model<Category>();
|
||||
if (UpdatedCategory is null) { DebugLogger.Log("[Realtime] Category UPDATE: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Category UPDATE: {UpdatedCategory.Id} ({UpdatedCategory.Name})");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var idx = Categories.ToList().FindIndex(x => x.Id == UpdatedCategory.Id);
|
||||
if (idx != -1) { Categories[idx] = UpdatedCategory; DebugLogger.Log($"[Realtime] Category UPDATE: replaced at index {idx}"); }
|
||||
else DebugLogger.Log($"[Realtime] Category UPDATE: id {UpdatedCategory.Id} not found in collection");
|
||||
});
|
||||
});
|
||||
|
||||
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
|
||||
{
|
||||
var deletedCategory = c.OldModel<Category>();
|
||||
if (deletedCategory is null) { DebugLogger.Log("[Realtime] Category DELETE: old model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Category DELETE: {deletedCategory.Id}");
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var item = Categories.FirstOrDefault(x => x.Id == deletedCategory.Id);
|
||||
if (item is not null) { Categories.Remove(item); DebugLogger.Log($"[Realtime] Category DELETE: removed {deletedCategory.Id}"); }
|
||||
else DebugLogger.Log($"[Realtime] Category DELETE: id {deletedCategory.Id} not found (already removed locally)");
|
||||
});
|
||||
});
|
||||
|
||||
// Profile
|
||||
_ = SupabaseService.Client.From<Profile>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
|
||||
{
|
||||
var updatedProfile = c.Model<Profile>();
|
||||
if (updatedProfile is null) { DebugLogger.Log("[Realtime] Profile UPDATE: model was null"); return; }
|
||||
DebugLogger.Log($"[Realtime] Profile UPDATE: {updatedProfile.Id} ({updatedProfile.DisplayName})");
|
||||
Dispatcher.UIThread.Post(() => Profile = updatedProfile);
|
||||
});
|
||||
|
||||
DebugLogger.Log("[Realtime] all listeners registered");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ──────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
@@ -57,7 +57,7 @@
|
||||
<Border Grid.Column="2" Width="36" />
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable form ───────────────────── -->
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -366,7 +366,7 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── Bottom action bar ─────────────────── -->
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- Root grid — content + overlay stacked -->
|
||||
<Grid>
|
||||
|
||||
<!-- ── Main content ───────────────────────── -->
|
||||
<!-- Main content -->
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Bottom sheet overlay ───────────────── -->
|
||||
<!-- Bottom sheet overlay -->
|
||||
<Grid IsVisible="False"
|
||||
x:Name="OverlayGrid">
|
||||
|
||||
@@ -526,7 +526,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Dialog overlays ───────────────────── -->
|
||||
<!-- Dialog overlays -->
|
||||
<mobileViews:DeleteAccountDialogViewMobile IsVisible="{Binding DataContext.IsDeleteDialogVisible, ElementName=AccountsPage}"
|
||||
DataContext="{Binding DeleteDialog}" />
|
||||
<mobileViews:ArchiveAccountDialogViewMobile IsVisible="{Binding IsArchiveDialogVisible}" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
@@ -7,6 +7,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Clario.Models;
|
||||
using Clario.ViewModels;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
@@ -28,6 +29,28 @@ public partial class AccountsViewMobile : UserControl
|
||||
{
|
||||
if (e.Source is Button { DataContext: Account }) await ShowSheet();
|
||||
}, handledEventsToo: false);
|
||||
|
||||
DataContextChanged += (_, _) =>
|
||||
{
|
||||
if (DataContext is AccountsViewModel vm)
|
||||
{
|
||||
vm.TryCloseSheet = () =>
|
||||
{
|
||||
if (!_sheetVisible) return false;
|
||||
_ = HideSheet();
|
||||
return true;
|
||||
};
|
||||
|
||||
vm.PropertyChanged += async (_, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(AccountsViewModel.ShouldCloseSheet) && vm.ShouldCloseSheet)
|
||||
{
|
||||
await HideSheet();
|
||||
vm.ShouldCloseSheet = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
@@ -84,6 +107,7 @@ public partial class AccountsViewMobile : UserControl
|
||||
public async Task HideSheet()
|
||||
{
|
||||
if (!_sheetVisible) return;
|
||||
_sheetVisible = false;
|
||||
|
||||
var sheetAnim = new Animation
|
||||
{
|
||||
@@ -110,9 +134,8 @@ public partial class AccountsViewMobile : UserControl
|
||||
|
||||
await Task.WhenAll(sheetAnim.RunAsync(BottomSheet), dimAnim.RunAsync(DimOverlay));
|
||||
|
||||
_sheetVisible = false;
|
||||
OverlayGrid.IsVisible = false;
|
||||
SheetTranslate.Y = 0;
|
||||
DimOverlay.Opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="24,48,24,48" Spacing="0">
|
||||
|
||||
<!-- ── Logo ──────────────────────────────── -->
|
||||
<!-- Logo -->
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
|
||||
<Border CornerRadius="16"
|
||||
Height="80"
|
||||
@@ -31,13 +31,14 @@
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Tab switcher ───────────────────────── -->
|
||||
<!-- Tab switcher -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,28">
|
||||
Margin="0,0,0,28"
|
||||
IsVisible="{Binding ShowTabs}">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -69,7 +70,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Sign In panel ──────────────────────── -->
|
||||
<!-- Sign In panel -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isSignin}">
|
||||
|
||||
<!-- Email -->
|
||||
@@ -150,6 +151,20 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Forgot password -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,8,0,24"
|
||||
Command="{Binding SetOperationCommand}"
|
||||
CommandParameter="forgotPassword">
|
||||
<TextBlock Text="Forgot password?"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</Button>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
@@ -191,7 +206,7 @@
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Create Account panel ───────────────── -->
|
||||
<!-- Create Account panel -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
|
||||
|
||||
<!-- First / Last name -->
|
||||
@@ -374,7 +389,127 @@
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Footer ──────────────────────────────── -->
|
||||
<!-- FORGOT PASSWORD PANEL -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isForgotPassword}">
|
||||
|
||||
<!-- Back button -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,24"
|
||||
Command="{Binding SetOperationCommand}"
|
||||
CommandParameter="login">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/arrow-left.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Back to Sign In"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<TextBlock Text="Reset your password"
|
||||
FontSize="20" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Margin="0,0,0,8" />
|
||||
<TextBlock Text="Enter your email and we'll send you a reset link."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,24" />
|
||||
|
||||
<!-- Success state -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,12"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding ResetEmailSent}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
|
||||
<TextBlock Text="Check your email for a reset link."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Email -->
|
||||
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6"
|
||||
IsVisible="{Binding !ResetEmailSent}" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding !ResetEmailSent}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/mail.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,10,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Watermark="you@example.com"
|
||||
Text="{Binding Email}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,20"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Send Reset Link button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,14"
|
||||
Margin="0,0,0,24"
|
||||
IsVisible="{Binding !ResetEmailSent}"
|
||||
Command="{Binding SendResetLinkCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/mail.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Send Reset Link"
|
||||
FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<Separator Margin="0,0,0,16" />
|
||||
<TextBlock Text="Your data is encrypted and synced securely."
|
||||
FontSize="12"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ──────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
@@ -71,7 +71,7 @@
|
||||
<Border Grid.Column="2" Width="36" IsVisible="{Binding !IsEditMode}" />
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable form ───────────────────── -->
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -292,7 +292,7 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── Bottom action bar ─────────────────── -->
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -330,7 +330,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete confirm sub-modal ─────────── -->
|
||||
<!-- Delete confirm sub-modal -->
|
||||
<Grid Grid.Row="0" Grid.RowSpan="3" IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
@@ -74,13 +74,13 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable content ────────────────── -->
|
||||
<!-- Scrollable content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,24" Spacing="14">
|
||||
|
||||
<!-- ── Period overview strip ─────────── -->
|
||||
<!-- Period overview strip -->
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
|
||||
<!-- Budgeted -->
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Overall progress bar ──────────── -->
|
||||
<!-- Overall progress bar -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -195,7 +195,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Budget cards list ─────────────── -->
|
||||
<!-- Budget cards list -->
|
||||
<ItemsControl ItemsSource="{Binding VisibleBudgets}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
@@ -322,7 +322,7 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- ── Spending breakdown chart ──────── -->
|
||||
<!-- Spending breakdown chart -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -364,7 +364,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Period progress ───────────────── -->
|
||||
<!-- Period progress -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -417,7 +417,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Savings goal ──────────────────── -->
|
||||
<!-- Savings goal -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
|
||||
168
Clario/MobileViews/CategoriesViewMobile.axaml
Normal file
168
Clario/MobileViews/CategoriesViewMobile.axaml
Normal file
@@ -0,0 +1,168 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.CategoriesViewMobile"
|
||||
x:DataType="vm:CategoriesViewModel"
|
||||
x:Name="CategoriesPage"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:CategoriesViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Categories"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
Padding="12,8"
|
||||
Command="{Binding AddCategoryCommand}">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,24" Spacing="20">
|
||||
|
||||
<!-- Expense section -->
|
||||
<StackPanel IsVisible="{Binding HasExpenseCategories}" Spacing="8">
|
||||
<TextBlock Text="EXPENSES" Classes="label" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1,0.25"
|
||||
CornerRadius="14"
|
||||
ClipToBounds="True">
|
||||
<ItemsControl ItemsSource="{Binding ExpenseCategories}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="0" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Category">
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0.75">
|
||||
<Button Classes="nav"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Padding="14,12"
|
||||
Command="{Binding DataContext.EditCategoryCommand, ElementName=CategoriesPage}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Svg Grid.Column="2"
|
||||
Path="../Assets/Icons/chevron-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Income section -->
|
||||
<StackPanel IsVisible="{Binding HasIncomeCategories}" Spacing="8">
|
||||
<TextBlock Text="INCOME" Classes="label" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1,0.25"
|
||||
CornerRadius="14"
|
||||
ClipToBounds="True">
|
||||
<ItemsControl ItemsSource="{Binding IncomeCategories}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="0" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Category">
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0.75">
|
||||
<Button Classes="nav"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Padding="14,12"
|
||||
Command="{Binding DataContext.EditCategoryCommand, ElementName=CategoriesPage}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Svg Grid.Column="2"
|
||||
Path="../Assets/Icons/chevron-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/CategoriesViewMobile.axaml.cs
Normal file
11
Clario/MobileViews/CategoriesViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class CategoriesViewMobile : UserControl
|
||||
{
|
||||
public CategoriesViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
304
Clario/MobileViews/DashboardSkeletonViewMobile.axaml
Normal file
304
Clario/MobileViews/DashboardSkeletonViewMobile.axaml
Normal file
@@ -0,0 +1,304 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:CompileBindings="False"
|
||||
x:Class="Clario.MobileViews.DashboardSkeletonViewMobile"
|
||||
Classes="mobile">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.skeleton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.85" IterationCount="INFINITE" PlaybackDirection="Alternate" FillMode="Both">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Opacity" Value="0.35" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- Top Bar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
|
||||
<StackPanel Grid.Column="0" Spacing="7">
|
||||
<Border Classes="skeleton" Height="22" Width="185" CornerRadius="7" />
|
||||
<Border Classes="skeleton" Height="12" Width="115" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="10" Margin="0,0,8,0" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="38" Height="36"
|
||||
CornerRadius="10" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,24" Spacing="14">
|
||||
|
||||
<!-- KPI Cards 2-col grid -->
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<!-- Income card -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,12"
|
||||
Margin="0,0,6,0">
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="10" Width="52" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton" Height="22" Width="90" CornerRadius="7" />
|
||||
<Border Classes="skeleton" Height="20" Width="65" CornerRadius="20" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Expenses card -->
|
||||
<Border Grid.Column="1"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,12"
|
||||
Margin="6,0,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="10" Width="62" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton" Height="22" Width="82" CornerRadius="7" />
|
||||
<Border Classes="skeleton" Height="20" Width="60" CornerRadius="20" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Savings Rate full-width -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,12">
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border Classes="skeleton" Width="22" Height="22" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="10" Width="88" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="7" VerticalAlignment="Center"
|
||||
HorizontalAlignment="Stretch" CornerRadius="4" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="36" Height="18"
|
||||
Margin="12,0,0,0" CornerRadius="6" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Spending by Category chart card -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<Border Classes="skeleton" Height="14" Width="160" />
|
||||
<Border Classes="skeleton" Height="11" Width="105" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="85" Height="28"
|
||||
CornerRadius="{StaticResource RadiusIcon}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Chart placeholder -->
|
||||
<Border Classes="skeleton" Height="180" HorizontalAlignment="Stretch" CornerRadius="8" />
|
||||
<!-- Category labels -->
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="2" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="3" Classes="skeleton" Height="10" Margin="4,0" HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
<!-- Amount labels -->
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="11" Width="30" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="34" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="2" Classes="skeleton" Height="11" Width="28" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="3" Classes="skeleton" Height="11" Width="32" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Recent Transactions card -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="5">
|
||||
<Border Classes="skeleton" Height="14" Width="150" />
|
||||
<Border Classes="skeleton" Height="11" Width="100" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="50" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Transaction rows inside rounded container -->
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
|
||||
<StackPanel Spacing="1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||
<Border Classes="skeleton" Height="12" Width="130" />
|
||||
<Border Classes="skeleton" Height="10" Width="88" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="52" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||
<Border Classes="skeleton" Height="12" Width="110" />
|
||||
<Border Classes="skeleton" Height="10" Width="75" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="48" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||
<Border Classes="skeleton" Height="12" Width="145" />
|
||||
<Border Classes="skeleton" Height="10" Width="95" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="55" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||
<Border Classes="skeleton" Height="12" Width="120" />
|
||||
<Border Classes="skeleton" Height="10" Width="82" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="50" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgSurface}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||
<Border Classes="skeleton" Height="12" Width="135" />
|
||||
<Border Classes="skeleton" Height="10" Width="90" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="46" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Budget Tracker card -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="5">
|
||||
<Border Classes="skeleton" Height="14" Width="110" />
|
||||
<Border Classes="skeleton" Height="11" Width="80" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="52" Height="24"
|
||||
CornerRadius="{StaticResource RadiusControl}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Budget items -->
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="88" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="65" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="68" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="58" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="98" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="11" Width="62" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="5" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Accounts Summary card -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,14">
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Spacing="5">
|
||||
<Border Classes="skeleton" Height="14" Width="75" />
|
||||
<Border Classes="skeleton" Height="11" Width="130" />
|
||||
</StackPanel>
|
||||
<!-- Account rows inside rounded container -->
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
|
||||
<StackPanel Spacing="1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="90" />
|
||||
<Border Classes="skeleton" Height="10" Width="55" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="58" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="70" />
|
||||
<Border Classes="skeleton" Height="10" Width="48" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="52" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Background="{DynamicResource BgBase}" Margin="12,10">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="34" Height="34"
|
||||
CornerRadius="9" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="82" />
|
||||
<Border Classes="skeleton" Height="10" Width="52" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="12" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Total Balance row -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="85" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="18" Width="95" CornerRadius="7" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/DashboardSkeletonViewMobile.axaml.cs
Normal file
11
Clario/MobileViews/DashboardSkeletonViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class DashboardSkeletonViewMobile : UserControl
|
||||
{
|
||||
public DashboardSkeletonViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Financial Overview"
|
||||
@@ -47,13 +47,13 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable content ────────────────── -->
|
||||
<!-- Scrollable content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,24" Spacing="14">
|
||||
|
||||
<!-- ── KPI cards ──────────────────────── -->
|
||||
<!-- KPI cards -->
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<!-- Income -->
|
||||
@@ -164,7 +164,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Spending by category chart ────── -->
|
||||
<!-- Spending by category chart -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -271,7 +271,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Recent transactions ───────────── -->
|
||||
<!-- Recent transactions -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -370,19 +370,30 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Budget tracker ────────────────── -->
|
||||
<!-- Budget tracker -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14,14">
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Budget Tracker"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" />
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Budget Tracker"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="Monthly limits" FontSize="11" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Classes="nav"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding NavigateToBudgetCommand}"
|
||||
IsVisible="{Binding HasBudgetData}"
|
||||
Padding="8,4">
|
||||
<TextBlock Text="View all" FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Panel>
|
||||
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}"
|
||||
@@ -434,24 +445,29 @@
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Spacing="10" Margin="0,32"
|
||||
Spacing="10" Margin="0,20"
|
||||
IsVisible="{Binding !HasBudgetData}">
|
||||
<Svg Path="../Assets/Icons/wallet.svg" Css="{DynamicResource SvgDisabled}"
|
||||
Height="36" Width="36" HorizontalAlignment="Center" />
|
||||
Height="32" Width="32" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="No budgets set"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Create budgets to track your spending limits."
|
||||
FontSize="12" Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap"
|
||||
TextAlignment="Center" MaxWidth="200" />
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Center"
|
||||
Padding="20,8"
|
||||
CornerRadius="10"
|
||||
Command="{Binding OpenAddBudgetCommand}">
|
||||
<TextBlock Text="Create Budget"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Accounts summary ──────────────── -->
|
||||
<!-- Accounts summary -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
<mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
|
||||
DataContext="{Binding SetSavingsGoalDialogViewModel}"
|
||||
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
|
||||
<!-- ── Content area ──────────────────────── -->
|
||||
<!-- Content area -->
|
||||
<ContentControl Grid.Row="0"
|
||||
Content="{Binding CurrentView}" />
|
||||
|
||||
<!-- ── Bottom tab bar ────────────────────── -->
|
||||
<!-- Bottom tab bar -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -109,19 +109,19 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Budget -->
|
||||
<!-- More -->
|
||||
<Button Grid.Column="4"
|
||||
Classes="nav"
|
||||
Classes.active="{Binding isOnBudget}"
|
||||
Command="{Binding GoToBudgetCommand}"
|
||||
Classes.active="{Binding isOnMore}"
|
||||
Command="{Binding GoToMoreCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,6">
|
||||
<StackPanel Spacing="4" HorizontalAlignment="Center">
|
||||
<Svg Path="../Assets/Icons/wallet.svg"
|
||||
<Svg Path="../Assets/Icons/ellipsis.svg"
|
||||
Width="22" Height="22"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Budget"
|
||||
<TextBlock Text="More"
|
||||
FontSize="10"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Clario.ViewModels;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
@@ -8,4 +12,26 @@ public partial class MainViewMobile : UserControl
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel != null)
|
||||
topLevel.BackRequested += OnBackRequested;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel != null)
|
||||
topLevel.BackRequested -= OnBackRequested;
|
||||
}
|
||||
|
||||
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainViewModel vm)
|
||||
e.Handled = vm.HandleBackNavigation();
|
||||
}
|
||||
}
|
||||
147
Clario/MobileViews/MoreViewMobile.axaml
Normal file
147
Clario/MobileViews/MoreViewMobile.axaml
Normal file
@@ -0,0 +1,147 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.MoreViewMobile"
|
||||
x:DataType="vm:MoreViewModel"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:MoreViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- Top bar -->
|
||||
<StackPanel Grid.Row="0" Margin="16,16,16,12">
|
||||
<TextBlock Text="More"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,24" Spacing="0">
|
||||
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1,0,1,0"
|
||||
CornerRadius="14"
|
||||
ClipToBounds="True">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Analytics -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,1">
|
||||
<Button Classes="nav"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Padding="16,0"
|
||||
MinHeight="58"
|
||||
Command="{Binding GoToAnalyticsCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgPurple}"
|
||||
CornerRadius="10"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,14,0">
|
||||
<Svg Path="../Assets/Icons/chart-no-axes-combined.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgPurple}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="Analytics"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Svg Grid.Column="2"
|
||||
Path="../Assets/Icons/chevron-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
<!-- Budget -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Button Classes="nav"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Padding="16,0"
|
||||
MinHeight="58"
|
||||
Command="{Binding GoToBudgetCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="10"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,14,0">
|
||||
<Svg Path="../Assets/Icons/wallet.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgGreen}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="Budget"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Svg Grid.Column="2"
|
||||
Path="../Assets/Icons/chevron-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
<!-- Categories -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Button Classes="nav"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Padding="16,0"
|
||||
MinHeight="58"
|
||||
Command="{Binding GoToCategoriesCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgBlue}"
|
||||
CornerRadius="10"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,14,0">
|
||||
<Svg Path="../Assets/Icons/list.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgBlue}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="Categories"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Svg Grid.Column="2"
|
||||
Path="../Assets/Icons/chevron-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/MoreViewMobile.axaml.cs
Normal file
11
Clario/MobileViews/MoreViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class MoreViewMobile : UserControl
|
||||
{
|
||||
public MoreViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
238
Clario/MobileViews/ResetPasswordViewMobile.axaml
Normal file
238
Clario/MobileViews/ResetPasswordViewMobile.axaml
Normal file
@@ -0,0 +1,238 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.ResetPasswordViewMobile"
|
||||
x:DataType="vm:ResetPasswordViewModel"
|
||||
Classes="mobile"
|
||||
Background="{DynamicResource BgBase}">
|
||||
<Design.DataContext>
|
||||
<vm:ResetPasswordViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="24,48,24,48" Spacing="0">
|
||||
|
||||
<!-- Logo -->
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="6" Margin="0,0,0,36">
|
||||
<Border CornerRadius="16"
|
||||
Height="80"
|
||||
Width="80"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8">
|
||||
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
|
||||
</Border>
|
||||
<TextBlock Text="Your personal finance tracker"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Set new password"
|
||||
FontSize="22" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
<TextBlock Text="Enter and confirm your new password below."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,32" />
|
||||
|
||||
<!-- SUCCESS STATE -->
|
||||
<StackPanel IsVisible="{Binding PasswordUpdated}" Spacing="16">
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/circle-check.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
|
||||
<TextBlock Text="Password updated successfully."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,14"
|
||||
Command="{Binding GoToSignInCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/log-in.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Sign In"
|
||||
FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- FORM STATE -->
|
||||
<StackPanel IsVisible="{Binding !PasswordUpdated}" Spacing="0">
|
||||
|
||||
<!-- New Password -->
|
||||
<TextBlock Text="NEW PASSWORD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/lock.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,10,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Watermark="At least 6 characters"
|
||||
Text="{Binding NewPassword}"
|
||||
PasswordChar="●"
|
||||
RevealPassword="{Binding #showNew.IsChecked}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2"
|
||||
Name="showNew"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Height="48"
|
||||
Padding="12,0"
|
||||
VerticalAlignment="Center">
|
||||
<ToggleButton.Styles>
|
||||
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
</ToggleButton.Styles>
|
||||
<Panel>
|
||||
<Svg Path="../Assets/Icons/eye.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding #showNew.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding !#showNew.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Panel>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<TextBlock Text="CONFIRM PASSWORD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/lock.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,10,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Watermark="Repeat your password"
|
||||
Text="{Binding ConfirmPassword}"
|
||||
PasswordChar="●"
|
||||
RevealPassword="{Binding #showConfirm.IsChecked}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2"
|
||||
Name="showConfirm"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Height="48"
|
||||
Padding="12,0"
|
||||
VerticalAlignment="Center">
|
||||
<ToggleButton.Styles>
|
||||
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
</ToggleButton.Styles>
|
||||
<Panel>
|
||||
<Svg Path="../Assets/Icons/eye.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding #showConfirm.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding !#showConfirm.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Panel>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,20"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Update password button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,14"
|
||||
Margin="0,0,0,24"
|
||||
Command="{Binding SetNewPasswordCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/lock.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Update Password"
|
||||
FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<Separator Margin="0,0,0,16" />
|
||||
<TextBlock Text="Your data is encrypted and synced securely."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/ResetPasswordViewMobile.axaml.cs
Normal file
11
Clario/MobileViews/ResetPasswordViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ResetPasswordViewMobile : UserControl
|
||||
{
|
||||
public ResetPasswordViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
@@ -26,13 +26,13 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable content ────────────────── -->
|
||||
<!-- Scrollable content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,32" Spacing="0">
|
||||
|
||||
<!-- ── Global success / error banners ── -->
|
||||
<!-- Global success / error banners -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
@@ -78,9 +78,9 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Profile
|
||||
══════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="PROFILE" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -201,9 +201,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Account & Security
|
||||
══════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="ACCOUNT & SECURITY" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -213,7 +213,7 @@
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Email row ──────────────────────── -->
|
||||
<!-- Email row -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="16,0">
|
||||
@@ -323,7 +323,7 @@
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Password row ───────────────────── -->
|
||||
<!-- Password row -->
|
||||
<Border Padding="16,0">
|
||||
<Panel>
|
||||
<!-- Display row -->
|
||||
@@ -445,9 +445,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Session
|
||||
══════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable form ────────────────────── -->
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -154,7 +154,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Amount ──────────────────────── -->
|
||||
<!-- Amount -->
|
||||
<TextBlock Text="AMOUNT" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -173,7 +173,7 @@
|
||||
Margin="0,0,8,0"
|
||||
Padding="0 0 0 2" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Classes="ghost numeric"
|
||||
Text="{Binding Amount, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="32"
|
||||
@@ -195,7 +195,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Description (hidden for transfers) ─── -->
|
||||
<!-- Description (hidden for transfers) -->
|
||||
<TextBlock Text="DESCRIPTION" Classes="label" FontSize="14" Margin="0,0,0,6"
|
||||
IsVisible="{Binding !IsTransfer}" />
|
||||
<TextBox Text="{Binding Description, Mode=TwoWay}"
|
||||
@@ -207,7 +207,7 @@
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}" />
|
||||
|
||||
<!-- ── Category + Account (income/expense) ── -->
|
||||
<!-- Category + Account (income/expense) -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}">
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── From + To accounts (transfer) ── -->
|
||||
<!-- From + To accounts (transfer) -->
|
||||
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding IsTransfer}">
|
||||
<!-- From Account -->
|
||||
@@ -367,7 +367,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Exchange Rate ──────────────────── -->
|
||||
<!-- Exchange Rate -->
|
||||
<Border IsVisible="{Binding ShowExchangeRateField}"
|
||||
Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -424,7 +424,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Date ────────────────────────── -->
|
||||
<!-- Date -->
|
||||
<TextBlock Text="DATE" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -456,7 +456,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Note ────────────────────────── -->
|
||||
<!-- Note -->
|
||||
<TextBlock Text="NOTE (OPTIONAL)" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Note, Mode=TwoWay}"
|
||||
Watermark="Add a note..."
|
||||
@@ -466,7 +466,7 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- ── Budget approaching warning ──────── -->
|
||||
<!-- Budget approaching warning -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
@@ -486,7 +486,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Budget over-limit warning ──────── -->
|
||||
<!-- Budget over-limit warning -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -506,7 +506,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -528,7 +528,7 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── Bottom action bar ──────────────────── -->
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -578,7 +578,7 @@
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -619,7 +619,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -630,7 +630,7 @@
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
x:DataType="vm:TransactionsViewModel"
|
||||
x:Name="transactionsRoot"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:TransactionsViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top Bar ───────────────────────────── -->
|
||||
<!-- Top Bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
@@ -169,7 +172,8 @@
|
||||
Padding="0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ApplyFiltersCommand}">
|
||||
Command="{Binding LoadPageStrCommand}"
|
||||
CommandParameter="1">
|
||||
<TextBlock Text="Apply"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
@@ -188,7 +192,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Summary strip ─────────────────────── -->
|
||||
<!-- Summary strip -->
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="*,*,*"
|
||||
Margin="16,12,16,0">
|
||||
@@ -271,13 +275,13 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Transaction list ──────────────────── -->
|
||||
<!-- Transaction list -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
Margin="0,12,0,0"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding FilteredTransactions}">
|
||||
<ItemsControl ItemsSource="{Binding PagedTransactions}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
@@ -391,6 +395,22 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Load More button -->
|
||||
<Button Classes="base"
|
||||
Margin="16,16,16,16"
|
||||
Padding="0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="13"
|
||||
IsVisible="{Binding HasNextPage}"
|
||||
Command="{Binding LoadMoreCommand}">
|
||||
<TextBlock Text="Load More"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
Spacing="12"
|
||||
|
||||
@@ -25,7 +25,7 @@ public class Budget : BaseModel
|
||||
|
||||
[Column("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
// ── not in DB ──────────────────────────────────────
|
||||
// not in DB
|
||||
|
||||
[JsonIgnore] public Category? Category { get; set; }
|
||||
[JsonIgnore] public int TransactionsCount { get; set; }
|
||||
|
||||
83
Clario/Services/DateRangeService.cs
Normal file
83
Clario/Services/DateRangeService.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Clario.Services;
|
||||
|
||||
/// <summary>Resolves a named date range option into concrete start/end dates and a display label.</summary>
|
||||
public static class DateRangeService
|
||||
{
|
||||
private static readonly CultureInfo Culture = new("en-US");
|
||||
|
||||
/// <param name="option">The named range key (e.g. "This Month", "Custom Range").</param>
|
||||
/// <param name="customDates">Required when option is "Custom Range".</param>
|
||||
/// <returns>Null Start/End means "All Time" (no filter). Label is already uppercased for display.</returns>
|
||||
public static (DateTime? Start, DateTime? End, string Label) Resolve(string option, IList<DateTime>? customDates = null)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
return option switch
|
||||
{
|
||||
"Today" => (now.Date, now.Date, now.ToString("MMM d, yyyy", Culture).ToUpper()),
|
||||
"This Week" => ResolveThisWeek(now),
|
||||
"This Month" => ResolveThisMonth(now),
|
||||
"Last Month" => ResolveLastMonth(now),
|
||||
"This Quarter" => ResolveThisQuarter(now),
|
||||
"This Year" => (new DateTime(now.Year, 1, 1), new DateTime(now.Year, 12, 31), now.Year.ToString()),
|
||||
"Custom Range" => ResolveCustomRange(customDates, now),
|
||||
_ => (null, null, "ALL TIME")
|
||||
};
|
||||
}
|
||||
|
||||
private static (DateTime?, DateTime?, string) ResolveThisWeek(DateTime now)
|
||||
{
|
||||
var start = now.Date.AddDays(-(int)now.DayOfWeek);
|
||||
return (start, start.AddDays(6), "THIS WEEK");
|
||||
}
|
||||
|
||||
private static (DateTime?, DateTime?, string) ResolveThisMonth(DateTime now)
|
||||
{
|
||||
var start = new DateTime(now.Year, now.Month, 1);
|
||||
return (start, start.AddMonths(1).AddDays(-1), now.ToString("MMMM yyyy", Culture).ToUpper());
|
||||
}
|
||||
|
||||
private static (DateTime?, DateTime?, string) ResolveLastMonth(DateTime now)
|
||||
{
|
||||
var lm = now.AddMonths(-1);
|
||||
var start = new DateTime(lm.Year, lm.Month, 1);
|
||||
return (start, start.AddMonths(1).AddDays(-1), lm.ToString("MMMM yyyy", Culture).ToUpper());
|
||||
}
|
||||
|
||||
private static (DateTime?, DateTime?, string) ResolveThisQuarter(DateTime now)
|
||||
{
|
||||
var quarterMonth = now.Month - ((now.Month - 1) % 3);
|
||||
var start = new DateTime(now.Year, quarterMonth, 1);
|
||||
var end = start.AddMonths(3).AddDays(-1);
|
||||
return (start, end, $"Q{(now.Month - 1) / 3 + 1} {now.Year}");
|
||||
}
|
||||
|
||||
private static (DateTime?, DateTime?, string) ResolveCustomRange(IList<DateTime>? dates, DateTime now)
|
||||
{
|
||||
if (dates is null || dates.Count == 0)
|
||||
return (now.Date, now.Date, now.ToString("MMM d, yyyy", Culture).ToUpper());
|
||||
|
||||
var ordered = dates.Select(d => d.Date).Distinct().OrderBy(d => d).ToList();
|
||||
var start = ordered.First();
|
||||
var end = ordered.Last();
|
||||
|
||||
var label = ordered.Count == 1
|
||||
? start.ToString("MMM dd, yyyy", Culture).ToUpper()
|
||||
: $"{start.ToString("MMM dd", Culture)} - {end.ToString("MMM dd, yyyy", Culture)}".ToUpper();
|
||||
|
||||
return (start, end, label);
|
||||
}
|
||||
|
||||
/// <summary>Formats a date as "Today - MMM dd", "Yesterday - MMM dd", or "MMM dd, yyyy".</summary>
|
||||
public static string FormatGroupHeader(DateTime date)
|
||||
{
|
||||
var now = DateTime.Now.Date;
|
||||
if (date.Date == now) return "Today — " + date.ToString("MMM dd", Culture);
|
||||
if (date.Date == now.AddDays(-1)) return "Yesterday — " + date.ToString("MMM dd", Culture);
|
||||
return date.ToString("MMM dd, yyyy", Culture);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Clario.Services;
|
||||
|
||||
public static class PdfExportService
|
||||
{
|
||||
// ── Print palette (readable on white paper) ───────────
|
||||
// Print palette (readable on white paper)
|
||||
private const string TextPrimary = "#111827";
|
||||
private const string TextSecondary = "#374151";
|
||||
private const string TextMuted = "#6B7280";
|
||||
@@ -80,7 +80,7 @@ public static class PdfExportService
|
||||
page.MarginVertical(1.5f, Unit.Centimetre);
|
||||
page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial"));
|
||||
|
||||
// ── Header ────────────────────────────────────
|
||||
// Header
|
||||
page.Header().Column(col =>
|
||||
{
|
||||
col.Item().Row(row =>
|
||||
@@ -103,10 +103,10 @@ public static class PdfExportService
|
||||
col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar);
|
||||
});
|
||||
|
||||
// ── Content ───────────────────────────────────
|
||||
// Content
|
||||
page.Content().PaddingTop(18).Column(col =>
|
||||
{
|
||||
// ─ KPI cards ─────────────────────────────
|
||||
// KPI cards
|
||||
col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary);
|
||||
col.Item().PaddingTop(6).Table(table =>
|
||||
{
|
||||
@@ -141,7 +141,7 @@ public static class PdfExportService
|
||||
|
||||
col.Item().Height(20);
|
||||
|
||||
// ─ Top categories ─────────────────────────
|
||||
// Top categories
|
||||
if (topCategories.Count > 0)
|
||||
{
|
||||
col.Item().Text("Top Spending Categories")
|
||||
@@ -189,7 +189,7 @@ public static class PdfExportService
|
||||
col.Item().Height(20);
|
||||
}
|
||||
|
||||
// ─ Transactions ───────────────────────────
|
||||
// Transactions
|
||||
col.Item().Text($"Transactions ({periodTxs.Count})")
|
||||
.FontSize(11).SemiBold().FontColor(TextPrimary);
|
||||
col.Item().PaddingTop(6).Table(table =>
|
||||
@@ -244,7 +244,7 @@ public static class PdfExportService
|
||||
});
|
||||
});
|
||||
|
||||
// ── Footer ────────────────────────────────────
|
||||
// Footer
|
||||
page.Footer().PaddingTop(6).BorderTop(1).BorderColor(Border).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Generated by Clario — Your personal finance tracker")
|
||||
|
||||
@@ -241,6 +241,20 @@
|
||||
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #E8622A; }</x:String>
|
||||
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #D4306A; }</x:String>
|
||||
|
||||
<!-- SVG FILL COLORS -->
|
||||
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #F4F5F8; }</x:String>
|
||||
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #0F1117; }</x:String>
|
||||
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #353A4A; }</x:String>
|
||||
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6B7080; }</x:String>
|
||||
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #A0A6B8; }</x:String>
|
||||
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #3B6AFF; }</x:String>
|
||||
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #18A86B; }</x:String>
|
||||
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #D4A012; }</x:String>
|
||||
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #E53535; }</x:String>
|
||||
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6B44E0; }</x:String>
|
||||
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #E8622A; }</x:String>
|
||||
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #D4306A; }</x:String>
|
||||
|
||||
<!-- Toggle Switch specific -->
|
||||
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#E2E4ED" />
|
||||
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#C8CCDE" />
|
||||
@@ -249,10 +263,9 @@
|
||||
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" />
|
||||
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" />
|
||||
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" />
|
||||
<Bitmap x:Key="key">path</Bitmap>
|
||||
|
||||
|
||||
<!-- logos -->
|
||||
<!-- logos -->
|
||||
<!-- Icon only, light bg -->
|
||||
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-light.svg</x:String>
|
||||
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-128.png</Bitmap>
|
||||
@@ -606,7 +619,7 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.inset">
|
||||
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusInset}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
@@ -1092,11 +1105,11 @@
|
||||
<Style Selector="TextBox:disabled">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextDisabled}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox:readonly">
|
||||
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
|
||||
</Style>
|
||||
<Style Selector="TextBox:readonly /template/ Border#PART_BorderElement">
|
||||
@@ -1181,7 +1194,7 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="ComboBox:disabled /template/ Border#Background">
|
||||
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
@@ -1422,12 +1435,12 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="cc|DateRangePicker:disabled /template/ Button#PART_Button /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource BgElevated}" />
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — On Track ─────────────────────── -->
|
||||
<!-- Budget Card — On Track -->
|
||||
<Style Selector="Border.budget-card">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
|
||||
@@ -1437,7 +1450,7 @@
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — Warning ──────────────────────── -->
|
||||
<!-- Budget Card — Warning -->
|
||||
<Style Selector="Border.budget-card-warning">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" />
|
||||
@@ -1447,7 +1460,7 @@
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Budget Card — Over Budget ─────────────────── -->
|
||||
<!-- Budget Card — Over Budget -->
|
||||
<Style Selector="Border.budget-card-over">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" />
|
||||
@@ -1457,13 +1470,13 @@
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Progress Bar — Yellow ─────────────────────── -->
|
||||
<!-- Progress Bar — Yellow -->
|
||||
<Style Selector="ProgressBar.yellow /template/ Border#PART_Indicator">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentYellow}" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Badge — Warning ───────────────────────────── -->
|
||||
<!-- Badge — Warning -->
|
||||
<Style Selector="Border.badge-warning">
|
||||
<Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" />
|
||||
@@ -1471,7 +1484,7 @@
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
</Style>
|
||||
|
||||
<!-- ── Badge — Over ──────────────────────────────── -->
|
||||
<!-- Badge — Over -->
|
||||
<Style Selector="Border.badge-over">
|
||||
<Setter Property="Background" Value="{DynamicResource BadgeBgRed}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<!-- ── FluentCalendarButton (header + prev/next) ──────── -->
|
||||
<!-- FluentCalendarButton (header + prev/next) -->
|
||||
<Style Selector="Button.FluentCalendarButton, Button#PART_HeaderButton, Button#PART_PreviousButton, Button#PART_NextButton">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
@@ -24,7 +24,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource BorderSubtle}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ── Calendar root ──────────────────────────────────── -->
|
||||
<!-- Calendar root -->
|
||||
<Style Selector="Calendar">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BgSurface}"/>
|
||||
@@ -32,7 +32,7 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ── CalendarItem (the main container) ─────────────── -->
|
||||
<!-- CalendarItem (the main container) -->
|
||||
<Style Selector="CalendarItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
@@ -154,7 +154,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ── CalendarDayButton ──────────────────────────────── -->
|
||||
<!-- CalendarDayButton -->
|
||||
<Style Selector="CalendarDayButton">
|
||||
<Setter Property="Width" Value="38"/>
|
||||
<Setter Property="Height" Value="38"/>
|
||||
@@ -243,7 +243,7 @@
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Style>
|
||||
|
||||
<!-- ── CalendarButton (month/year picker cells) ──────── -->
|
||||
<!-- CalendarButton (month/year picker cells) -->
|
||||
<Style Selector="CalendarButton">
|
||||
<Setter Property="Width" Value="60"/>
|
||||
<Setter Property="Height" Value="52"/>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
In WinUI Min-Width from TemplateSettings
|
||||
basically...MinWidth of DayItem = 40, 40 * 7 = 280 + margins/padding = ~294
|
||||
Viewport height is set from # of rows displayed (2-8) in Month mode, = ~290 for 6 weeks (+ day names)
|
||||
-->
|
||||
-->
|
||||
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="40,*" MinWidth="294"
|
||||
Background="{DynamicResource BgSidebar}">
|
||||
<Grid ColumnDefinitions="5*,*,*">
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
RESOURCE OVERRIDES
|
||||
These override the Fluent resource keys used internally
|
||||
by the ColorPicker flyout template.
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Styles.Resources>
|
||||
<!-- Tab strip background (top 48px bar) -->
|
||||
<SolidColorBrush x:Key="SystemControlBackgroundBaseLowBrush" Color="#13161E"/>
|
||||
@@ -56,9 +56,9 @@
|
||||
<SolidColorBrush x:Key="ColorViewPreviewBorderBrush" Color="#1E2330"/>
|
||||
</Styles.Resources>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
ColorPicker — the drop-down button itself
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="ColorPicker">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
@@ -86,9 +86,9 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
Flyout popup wrapper
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="FlyoutPresenter.nopadding">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
@@ -98,9 +98,9 @@
|
||||
<!-- <Setter Property="BoxShadow" Value="0 8 32 0 #3C000000"/> -->
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
Tab strip inside the flyout
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabControl">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
</Style>
|
||||
@@ -129,9 +129,9 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
Hex input TextBox
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
@@ -147,9 +147,9 @@
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
NumericUpDown (RGB/HSV component value inputs)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup NumericUpDown">
|
||||
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
|
||||
@@ -161,9 +161,9 @@
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
ColorSlider (hue, saturation, value sliders)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="primitives|ColorSlider">
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Height" Value="16"/>
|
||||
@@ -193,9 +193,9 @@
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
<!--
|
||||
ColorPreviewer (accent color swatches at the bottom)
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Style Selector="primitives|ColorPreviewer">
|
||||
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
|
||||
</Style>
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<!-- DisabledState -->
|
||||
<!-- DisabledState -->
|
||||
<Style Selector="^:disabled">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextDisabled}" />
|
||||
</Style>
|
||||
@@ -155,7 +155,7 @@
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
|
||||
<!-- CheckedState -->
|
||||
<!-- CheckedState -->
|
||||
<Style Selector="^:checked /template/ Border#OuterBorder">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
@@ -180,7 +180,7 @@
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Style>
|
||||
|
||||
<!-- UncheckedState -->
|
||||
<!-- UncheckedState -->
|
||||
<Style Selector="^:unchecked /template/ Border#OuterBorder">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Style>
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class AccountFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
// Mode
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class AccountFormViewModel : ViewModelBase
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Account";
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
// Fields
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _name = "";
|
||||
|
||||
@@ -78,12 +78,12 @@ public partial class AccountFormViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private string _selectedColor = "#3B82F6";
|
||||
|
||||
// ── Options ─────────────────────────────────────────────
|
||||
// Options
|
||||
[ObservableProperty] private List<string> _accountTypes = new() { "Cash", "Checking", "Savings", "Credit", "Investment", "Other" };
|
||||
|
||||
[ObservableProperty] private List<string> _icons = new() { "wallet", "credit-card", "banknote", "landmark", "piggy-bank", "dollar-sign" };
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
@@ -95,17 +95,17 @@ public partial class AccountFormViewModel : ViewModelBase
|
||||
|
||||
public bool IsCredit => SelectedType == "Credit";
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
|
||||
// ── Edit mode: original account ─────────────────────────
|
||||
// Edit mode: original account
|
||||
private Guid? _editingId;
|
||||
|
||||
// ── Result account ──────────────────────────────────────
|
||||
// Result account
|
||||
public Account? ResultAccount { get; set; }
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
partial void OnSelectedTypeChanged(string value)
|
||||
{
|
||||
@@ -220,7 +220,7 @@ public partial class AccountFormViewModel : ViewModelBase
|
||||
OnCancelled?.Invoke();
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
// Public setup methods
|
||||
|
||||
/// <summary>Call this to open the form for adding a new account.</summary>
|
||||
public void SetupForAdd()
|
||||
|
||||
@@ -35,10 +35,15 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
[ObservableProperty] private List<Account> _archivedAccounts = new();
|
||||
public bool HasArchivedAccounts => ArchivedAccounts.Count > 0;
|
||||
|
||||
[ObservableProperty] private bool _shouldCloseSheet;
|
||||
|
||||
/// <summary>Set by AccountsViewMobile. Returns true and closes the sheet if it was open.</summary>
|
||||
public Func<bool>? TryCloseSheet { get; set; }
|
||||
|
||||
public AccountsViewModel()
|
||||
{
|
||||
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); };
|
||||
AppData.Transactions.CollectionChanged += (_, _) => { Initialize(); };
|
||||
Track(AppData.Accounts, (_, _) => Initialize());
|
||||
Track(AppData.Transactions, (_, _) => Initialize());
|
||||
Initialize();
|
||||
}
|
||||
|
||||
@@ -184,6 +189,7 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
{
|
||||
IsDeleteDialogVisible = false;
|
||||
Initialize();
|
||||
ShouldCloseSheet = true;
|
||||
};
|
||||
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
|
||||
IsDeleteDialogVisible = true;
|
||||
|
||||
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.SkiaSharpView;
|
||||
using LiveChartsCore.SkiaSharpView.Painting;
|
||||
using SkiaSharp;
|
||||
using SKColor = SkiaSharp.SKColor;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
@@ -21,7 +22,9 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
// ── Period ───────────────────────────────────────────
|
||||
private static readonly SKTypeface _interTypeface = SKTypeface.FromFamilyName("Inter");
|
||||
|
||||
// Period
|
||||
public List<string> PeriodOptions { get; } = new()
|
||||
{
|
||||
"Last 30 Days", "Last 3 Months", "Last 6 Months", "Last 12 Months", "This Year"
|
||||
@@ -31,7 +34,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
|
||||
partial void OnSelectedPeriodChanged(string value) => Initialize();
|
||||
|
||||
// ── KPI cards ────────────────────────────────────────
|
||||
// KPI cards
|
||||
[ObservableProperty] private string _totalIncomeFormatted = "—";
|
||||
[ObservableProperty] private string _totalExpensesFormatted = "—";
|
||||
[ObservableProperty] private string _netSavingsFormatted = "—";
|
||||
@@ -40,38 +43,38 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
|
||||
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
|
||||
// ── Cash Flow chart ──────────────────────────────────
|
||||
// Cash Flow chart
|
||||
[ObservableProperty] private ISeries[] _cashFlowSeries = [];
|
||||
[ObservableProperty] private Axis[] _cashFlowXAxes = [];
|
||||
[ObservableProperty] private Axis[] _cashFlowYAxes = [];
|
||||
|
||||
// ── Net Worth chart ──────────────────────────────────
|
||||
// Net Worth chart
|
||||
[ObservableProperty] private ISeries[] _netWorthSeries = [];
|
||||
[ObservableProperty] private Axis[] _netWorthXAxes = [];
|
||||
[ObservableProperty] private Axis[] _netWorthYAxes = [];
|
||||
|
||||
// ── Day-of-week chart ────────────────────────────────
|
||||
// Day-of-week chart
|
||||
[ObservableProperty] private ISeries[] _dayOfWeekSeries = [];
|
||||
[ObservableProperty] private Axis[] _dayOfWeekXAxes = [];
|
||||
|
||||
// ── Top categories ───────────────────────────────────
|
||||
// Top categories
|
||||
[ObservableProperty] private ObservableCollection<CategorySpendRow> _topCategories = new();
|
||||
[ObservableProperty] private bool _hasTopCategories;
|
||||
|
||||
// ── Income sources donut ─────────────────────────────
|
||||
// Income sources donut
|
||||
[ObservableProperty] private ISeries[] _incomeSourcesSeries = [];
|
||||
[ObservableProperty] private bool _hasIncomeSources;
|
||||
|
||||
// ── State ────────────────────────────────────────────
|
||||
// State
|
||||
[ObservableProperty] private bool _isExporting;
|
||||
[ObservableProperty] private string? _exportStatusMessage;
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
public AnalyticsViewModel()
|
||||
{
|
||||
AppData.Transactions.CollectionChanged += (_, _) => Initialize();
|
||||
AppData.Accounts.CollectionChanged += (_, _) => Initialize();
|
||||
Track(AppData.Transactions, (_, _) => Initialize());
|
||||
Track(AppData.Accounts, (_, _) => Initialize());
|
||||
Initialize();
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Date range ────────────────────────────────────────
|
||||
// Date range
|
||||
|
||||
private (DateTime start, DateTime end) GetDateRange()
|
||||
{
|
||||
@@ -132,7 +135,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
return buckets;
|
||||
}
|
||||
|
||||
// ── Section 1: KPIs ───────────────────────────────────
|
||||
// Section 1: KPIs
|
||||
|
||||
private void ComputeKpis(List<Transaction> income, List<Transaction> expenses)
|
||||
{
|
||||
@@ -150,7 +153,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
: "—";
|
||||
}
|
||||
|
||||
// ── Section 2: Cash Flow ──────────────────────────────
|
||||
// Section 2: Cash Flow
|
||||
|
||||
private void BuildCashFlowChart(DateTime start, DateTime end)
|
||||
{
|
||||
@@ -202,7 +205,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
new Axis
|
||||
{
|
||||
Labels = labels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
@@ -214,7 +217,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 10,
|
||||
@@ -223,7 +226,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
];
|
||||
}
|
||||
|
||||
// ── Section 3: Net Worth ──────────────────────────────
|
||||
// Section 3: Net Worth
|
||||
|
||||
private void BuildNetWorthChart(DateTime start, DateTime end)
|
||||
{
|
||||
@@ -266,7 +269,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
new Axis
|
||||
{
|
||||
Labels = labels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
@@ -278,7 +281,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 10,
|
||||
@@ -287,7 +290,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
];
|
||||
}
|
||||
|
||||
// ── Section 4: Day of Week ────────────────────────────
|
||||
// Section 4: Day of Week
|
||||
|
||||
private void BuildDayOfWeekChart(List<Transaction> expenses, DateTime start, DateTime end)
|
||||
{
|
||||
@@ -331,7 +334,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
new Axis
|
||||
{
|
||||
Labels = dayLabels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")),
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = null,
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
@@ -339,7 +342,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
];
|
||||
}
|
||||
|
||||
// ── Section 5: Top Categories ─────────────────────────
|
||||
// Section 5: Top Categories
|
||||
|
||||
private void BuildTopCategories(List<Transaction> expenses)
|
||||
{
|
||||
@@ -372,7 +375,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
HasTopCategories = grouped.Count > 0;
|
||||
}
|
||||
|
||||
// ── Section 6: Income Sources ─────────────────────────
|
||||
// Section 6: Income Sources
|
||||
|
||||
private void BuildIncomeSourcesChart(List<Transaction> income)
|
||||
{
|
||||
@@ -403,7 +406,7 @@ public partial class AnalyticsViewModel : ViewModelBase
|
||||
HasIncomeSources = true;
|
||||
}
|
||||
|
||||
// ── PDF Export ────────────────────────────────────────
|
||||
// PDF Export
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportPdf()
|
||||
|
||||
@@ -24,7 +24,7 @@ public partial class AuthViewModel : ViewModelBase
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
|
||||
private string _lastName;
|
||||
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
|
||||
private string _email;
|
||||
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
||||
@@ -34,13 +34,15 @@ public partial class AuthViewModel : ViewModelBase
|
||||
private string _confirmPassword;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
||||
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount), nameof(isForgotPassword), nameof(ShowTabs))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand), nameof(SendResetLinkCommand))]
|
||||
private string _operation = "login";
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
[ObservableProperty] private bool _resetEmailSent;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
public AuthViewModel()
|
||||
@@ -72,6 +74,30 @@ public partial class AuthViewModel : ViewModelBase
|
||||
{
|
||||
Operation = operation;
|
||||
ErrorMessage = null;
|
||||
ResetEmailSent = false;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(canSendResetLink))]
|
||||
private async Task SendResetLink()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.Auth.ResetPasswordForEmail(_email);
|
||||
ResetEmailSent = true;
|
||||
}
|
||||
catch (GotrueException e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = e.Reason == FailureHint.Reason.UserBadEmailAddress
|
||||
? GetErrorMessage(AuthError.InvalidEmail)
|
||||
: GetErrorMessage(AuthError.Unknown);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = GetErrorMessage(AuthError.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(canSignin))]
|
||||
@@ -185,12 +211,16 @@ public partial class AuthViewModel : ViewModelBase
|
||||
|
||||
public bool isSignin => Operation == "login";
|
||||
public bool isCreateAccount => Operation == "signup";
|
||||
public bool isForgotPassword => Operation == "forgotPassword";
|
||||
public bool ShowTabs => !isForgotPassword;
|
||||
|
||||
public bool canSignin => isSignin && !string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_password);
|
||||
|
||||
public bool canCreateAccount => isCreateAccount && !string.IsNullOrWhiteSpace(_firstName) && !string.IsNullOrWhiteSpace(_lastName) &&
|
||||
!string.IsNullOrWhiteSpace(_email) &&
|
||||
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
|
||||
|
||||
public bool canSendResetLink => isForgotPassword && !string.IsNullOrWhiteSpace(_email);
|
||||
}
|
||||
|
||||
class Wrapper
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class BudgetFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
// Mode
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class BudgetFormViewModel : ViewModelBase
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Budget";
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
// Fields
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsMonthly), nameof(IsQuarterly), nameof(IsYearly), nameof(IsValid))]
|
||||
private string _period = "monthly";
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class BudgetFormViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private bool _rollover = false;
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
@@ -57,20 +57,20 @@ public partial class BudgetFormViewModel : ViewModelBase
|
||||
decimal.TryParse(LimitAmount, out var amt) && amt > 0 &&
|
||||
SelectedCategory is not null;
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
public Action? OnDeleted;
|
||||
|
||||
[ObservableProperty] private bool _showDeleteConfirm = false;
|
||||
|
||||
// ── Edit mode: original budget ───────────────────────────
|
||||
// Edit mode: original budget
|
||||
private Guid? _editingId;
|
||||
|
||||
// ── Result ──────────────────────────────────────────────
|
||||
// Result
|
||||
public Budget? ResultBudget { get; set; }
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void SetPeriod(string period)
|
||||
@@ -174,7 +174,7 @@ public partial class BudgetFormViewModel : ViewModelBase
|
||||
OnCancelled?.Invoke();
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
// Public setup methods
|
||||
|
||||
/// <summary>Call this to open the form for adding a new budget.</summary>
|
||||
public void SetupForAdd(ObservableCollection<Category> categories)
|
||||
|
||||
@@ -69,17 +69,20 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
|
||||
public BudgetViewModel()
|
||||
{
|
||||
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
|
||||
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
|
||||
AppData.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(AppData.Profile))
|
||||
NotifyComputedPropertiesOnChanged();
|
||||
};
|
||||
Track(AppData.Budgets, async (_, _) => await Initialize());
|
||||
Track(AppData.Transactions, async (_, _) => await Initialize());
|
||||
AppData.PropertyChanged += OnProfileChanged;
|
||||
OnDispose(() => AppData.PropertyChanged -= OnProfileChanged);
|
||||
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize());
|
||||
_ = Initialize();
|
||||
}
|
||||
|
||||
private void OnProfileChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AppData.Profile))
|
||||
NotifyComputedPropertiesOnChanged();
|
||||
}
|
||||
|
||||
private async Task Initialize()
|
||||
{
|
||||
try
|
||||
@@ -98,19 +101,19 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CreateBudget()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null);
|
||||
if (parentViewModel is MainViewModel main) main.OpenAddBudgetCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditBudget(Budget budget)
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
|
||||
if (parentViewModel is MainViewModel main) main.OpenEditBudgetCommand.Execute(budget);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditSavingsGoal()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenEditSavingsGoalCommand.Execute(null);
|
||||
if (parentViewModel is MainViewModel main) main.OpenEditSavingsGoalCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void ProcessChartData()
|
||||
|
||||
54
Clario/ViewModels/CategoriesViewModel.cs
Normal file
54
Clario/ViewModels/CategoriesViewModel.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class CategoriesViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasExpenseCategories))]
|
||||
private ObservableCollection<Category> _expenseCategories = new();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasIncomeCategories))]
|
||||
private ObservableCollection<Category> _incomeCategories = new();
|
||||
|
||||
public bool HasExpenseCategories => ExpenseCategories.Count > 0;
|
||||
public bool HasIncomeCategories => IncomeCategories.Count > 0;
|
||||
|
||||
public CategoriesViewModel()
|
||||
{
|
||||
Track(AppData.Categories, (_, _) => Initialize());
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
ExpenseCategories = new ObservableCollection<Category>(
|
||||
AppData.Categories.Where(c => c.Type == "expense").OrderBy(c => c.Name));
|
||||
IncomeCategories = new ObservableCollection<Category>(
|
||||
AppData.Categories.Where(c => c.Type == "income").OrderBy(c => c.Name));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditCategory(Category category)
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainVm)
|
||||
mainVm.OpenEditCategory(category);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddCategory()
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainVm)
|
||||
mainVm.OpenAddCategory();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
// Mode
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))]
|
||||
private bool _isEditMode = false;
|
||||
@@ -22,7 +22,7 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category";
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
// Fields
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _name = "";
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private string _selectedColor = "#7B9CFF";
|
||||
|
||||
// ── Icon options ─────────────────────────────────────────
|
||||
// Icon options
|
||||
public List<string> CategoryIcons { get; } = new()
|
||||
{
|
||||
// Food & Dining
|
||||
@@ -59,7 +59,7 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
"receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw",
|
||||
};
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
@@ -69,18 +69,18 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
|
||||
public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4;
|
||||
|
||||
// ── Delete confirm sub-modal ────────────────────────────
|
||||
// Delete confirm sub-modal
|
||||
[ObservableProperty] private bool _showDeleteConfirm = false;
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
public Action? OnDeleted;
|
||||
|
||||
// ── Edit mode: original category ────────────────────────
|
||||
// Edit mode: original category
|
||||
private Guid? _editingId;
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void SetType(string type) => Type = type;
|
||||
@@ -163,7 +163,7 @@ public partial class CategoryFormViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
// Public setup methods
|
||||
|
||||
public void SetupForAdd()
|
||||
{
|
||||
|
||||
5
Clario/ViewModels/DashboardSkeletonViewModel.cs
Normal file
5
Clario/ViewModels/DashboardSkeletonViewModel.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class DashboardSkeletonViewModel : ViewModelBase
|
||||
{
|
||||
}
|
||||
@@ -86,38 +86,25 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
|
||||
partial void OnSelectedChartTimePeriodChanged(string value)
|
||||
{
|
||||
ChartTimePeriod period = value switch
|
||||
{
|
||||
"This Month" => ChartTimePeriod.ThisMonth,
|
||||
"Last Month" => ChartTimePeriod.LastMonth,
|
||||
"This Quarter" => ChartTimePeriod.ThisQuarter,
|
||||
"This Year" => ChartTimePeriod.ThisYear,
|
||||
_ => ChartTimePeriod.ThisMonth
|
||||
};
|
||||
var (_, _, subtitle) = DateRangeService.Resolve(value);
|
||||
SelectedChartTimPeriodSubTitle = subtitle.Length > 0
|
||||
? char.ToUpper(subtitle[0]) + subtitle.Substring(1).ToLower()
|
||||
: subtitle;
|
||||
|
||||
SelectedChartTimPeriodSubTitle = value switch
|
||||
{
|
||||
"This Month" => DateTime.Now.ToString("MMMM yyyy"),
|
||||
"Last Month" => DateTime.Now.AddMonths(-1).ToString("MMMM yyyy"),
|
||||
"This Quarter" => $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}",
|
||||
"This Year" => DateTime.Now.Year.ToString(),
|
||||
_ => DateTime.Now.ToString("MMMM yyyy")
|
||||
};
|
||||
|
||||
UpdateSpendingByCategoryChart(period);
|
||||
UpdateSpendingByCategoryChart(value);
|
||||
}
|
||||
|
||||
public DashboardViewModel()
|
||||
{
|
||||
AppData.Transactions.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Accounts.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Categories.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
AppData.Budgets.CollectionChanged += (s, e) => UpdateUserOverview();
|
||||
Track(AppData.Transactions, (_, _) => UpdateUserOverview());
|
||||
Track(AppData.Accounts, (_, _) => UpdateUserOverview());
|
||||
Track(AppData.Categories, (_, _) => UpdateUserOverview());
|
||||
Track(AppData.Budgets, (_, _) => UpdateUserOverview());
|
||||
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview());
|
||||
initialize();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void initialize()
|
||||
public void Initialize()
|
||||
{
|
||||
UpdateUserOverview();
|
||||
}
|
||||
@@ -126,7 +113,7 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
private void UpdateUserOverview()
|
||||
{
|
||||
CalculateMonthlyValues();
|
||||
UpdateSpendingByCategoryChart();
|
||||
UpdateSpendingByCategoryChart(SelectedChartTimePeriod);
|
||||
_ = UpdateBudgetTracker();
|
||||
UpdateRecentTransactions();
|
||||
UpdateAccountsSummary();
|
||||
@@ -175,53 +162,50 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CreateTransaction()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddTransaction();
|
||||
if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateToSettings()
|
||||
{
|
||||
((MainViewModel)parentViewModel).GoToSettingsCommand.Execute(null);
|
||||
if (parentViewModel is MainViewModel main) main.GoToSettingsCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void UpdateSpendingByCategoryChart(ChartTimePeriod period = ChartTimePeriod.ThisMonth)
|
||||
[RelayCommand]
|
||||
private void NavigateToBudget()
|
||||
{
|
||||
if (parentViewModel is MainViewModel main) main.GoToBudgetCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenAddBudget()
|
||||
{
|
||||
if (parentViewModel is MainViewModel main) main.OpenAddBudgetCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void UpdateSpendingByCategoryChart(string period = "This Month")
|
||||
{
|
||||
var (start, end, _) = DateRangeService.Resolve(period);
|
||||
var tempList = new List<ColumnChartData>();
|
||||
|
||||
foreach (var category in AppData.Categories)
|
||||
{
|
||||
var categoryTransactions =
|
||||
AppData.Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase));
|
||||
var txns = AppData.Transactions
|
||||
.Where(x => x.CategoryId == category.Id
|
||||
&& x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase)
|
||||
&& (start is null || x.Date.Date >= start.Value)
|
||||
&& (end is null || x.Date.Date <= end.Value));
|
||||
|
||||
switch (period)
|
||||
var total = txns.Sum(x => x.ConvertedAmount);
|
||||
if (total == 0) continue;
|
||||
|
||||
tempList.Add(new ColumnChartData
|
||||
{
|
||||
case ChartTimePeriod.ThisMonth:
|
||||
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.Month);
|
||||
break;
|
||||
|
||||
case ChartTimePeriod.LastMonth:
|
||||
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.AddMonths(-1).Month);
|
||||
break;
|
||||
|
||||
case ChartTimePeriod.ThisQuarter:
|
||||
categoryTransactions = categoryTransactions.Where(x =>
|
||||
x.Date.Month >= DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3).Month &&
|
||||
x.Date.Month <= DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3).AddMonths(3).Month);
|
||||
break;
|
||||
|
||||
case ChartTimePeriod.ThisYear:
|
||||
categoryTransactions = categoryTransactions.Where(x => x.Date.Year == DateTime.Now.Year);
|
||||
break;
|
||||
|
||||
default:
|
||||
categoryTransactions = categoryTransactions.Where(x => x.Date.Month == DateTime.Now.Month);
|
||||
break;
|
||||
}
|
||||
|
||||
var balance = categoryTransactions.Sum(x => x.ConvertedAmount);
|
||||
if (balance == 0) continue;
|
||||
tempList.Add(new ColumnChartData()
|
||||
{ id = category.Id, Name = category.Name, Values = [(double)balance], Fill = new SolidColorPaint(SKColor.Parse(category.Color)) });
|
||||
id = category.Id,
|
||||
Name = category.Name,
|
||||
Values = [(double)total],
|
||||
Fill = new SolidColorPaint(SKColor.Parse(category.Color))
|
||||
});
|
||||
}
|
||||
|
||||
tempList = tempList.OrderByDescending(x => x.Values[0]).ToList();
|
||||
@@ -268,12 +252,4 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.Where(a => !a.IsArchived).OrderBy(x => x.CreatedAt));
|
||||
OnPropertyChanged(nameof(AccountsSubtitle));
|
||||
}
|
||||
|
||||
private enum ChartTimePeriod
|
||||
{
|
||||
ThisMonth,
|
||||
LastMonth,
|
||||
ThisQuarter,
|
||||
ThisYear
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Clario.ViewModels;
|
||||
|
||||
public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
{
|
||||
// ── State machine ────────────────────────────────────────
|
||||
// State machine
|
||||
public enum DialogStep
|
||||
{
|
||||
SimpleConfirm,
|
||||
@@ -31,7 +31,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
public bool IsHasTransactionsStep => CurrentStep == DialogStep.HasTransactions;
|
||||
public bool IsMigrateStep => CurrentStep == DialogStep.Migrate;
|
||||
|
||||
// ── Data ─────────────────────────────────────────────────
|
||||
// Data
|
||||
[ObservableProperty] private Account? _account;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
@@ -40,7 +40,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Account> _availableAccounts = new();
|
||||
|
||||
// ── Validation ───────────────────────────────────────────
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
@@ -50,11 +50,11 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
TargetAccount is not null &&
|
||||
TargetAccount.Id != Account?.Id;
|
||||
|
||||
// ── Callbacks ────────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnDeleted;
|
||||
public Action? OnCancelled;
|
||||
|
||||
// ── Setup ────────────────────────────────────────────────
|
||||
// Setup
|
||||
|
||||
/// <summary>
|
||||
/// Call this to open the dialog for a specific account.
|
||||
@@ -79,7 +79,7 @@ public partial class DeleteAccountDialogViewModel : ViewModelBase
|
||||
: DialogStep.SimpleConfirm;
|
||||
}
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => OnCancelled?.Invoke();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using Avalonia.Xaml.Interactivity;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class LoadingViewModel : ViewModelBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -21,7 +21,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
public TransactionsViewModel _transactionsViewModel = null!;
|
||||
private AccountsViewModel _accountsViewModel = null!;
|
||||
private BudgetViewModel _budgetViewModel = null!;
|
||||
private CategoriesViewModel _categoriesViewModel = null!;
|
||||
private AnalyticsViewModel _analyticsViewModel = null!;
|
||||
private MoreViewModel _moreViewModel = null!;
|
||||
|
||||
GeneralDataRepo AppData => DataRepo.General;
|
||||
[ObservableProperty] private Profile? _profile;
|
||||
@@ -34,6 +36,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
[ObservableProperty] private SetSavingsGoalDialogViewModel _setSavingsGoalDialogViewModel = null!;
|
||||
|
||||
[ObservableProperty] private bool _isDimmed;
|
||||
[ObservableProperty] private bool _isMessageBoxVisible;
|
||||
[ObservableProperty] private MessageBoxViewModel _messageBoxViewModel = new();
|
||||
|
||||
[ObservableProperty] private bool _isTransactionFormVisible;
|
||||
[ObservableProperty] private bool _isAccountFormVisible;
|
||||
[ObservableProperty] private bool _isBudgetFormVisible;
|
||||
@@ -42,7 +47,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnAnalytics), nameof(isOnSettings))]
|
||||
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), nameof(isOnCategories), nameof(isOnAnalytics),
|
||||
nameof(isOnSettings), nameof(isOnMore))]
|
||||
private ViewModelBase? _currentView;
|
||||
|
||||
[ObservableProperty] private bool _isDarkTheme;
|
||||
@@ -50,13 +56,13 @@ public partial class MainViewModel : ViewModelBase
|
||||
public MainViewModel()
|
||||
{
|
||||
DebugLogger.Log("main vm loaded");
|
||||
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (_, m) =>
|
||||
WeakReferenceMessenger.Default.Register<ProfileUpdated>(this, (s, m) =>
|
||||
{
|
||||
Profile = AppData.Profile;
|
||||
_ = DataRepo.General.RefreshLiveRatesAndEnrich();
|
||||
});
|
||||
IsDimmed = true;
|
||||
CurrentView = new LoadingViewModel();
|
||||
CurrentView = new DashboardSkeletonViewModel();
|
||||
_ = InitializeApp();
|
||||
}
|
||||
|
||||
@@ -73,12 +79,12 @@ public partial class MainViewModel : ViewModelBase
|
||||
var accountsTask = DataRepo.General.FetchAccounts();
|
||||
var budgetsTask = DataRepo.General.FetchBudgets();
|
||||
await Task.WhenAll(profilesTask, categoriesTask, accountsTask, transactionsTask, budgetsTask);
|
||||
|
||||
Profile = profilesTask.Result;
|
||||
|
||||
DataRepo.General.LinkTransactionCategories();
|
||||
await DataRepo.General.RefreshLiveRatesAndEnrich();
|
||||
|
||||
|
||||
DebugLogger.Log("fetched all data");
|
||||
});
|
||||
|
||||
@@ -89,12 +95,13 @@ public partial class MainViewModel : ViewModelBase
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized DashboardViewModel");
|
||||
|
||||
_transactionsViewModel = new TransactionsViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
|
||||
DebugLogger.Log("initialized TransactionsViewModel");
|
||||
|
||||
_accountsViewModel = new AccountsViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
@@ -106,6 +113,16 @@ public partial class MainViewModel : ViewModelBase
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized BudgetViewModel");
|
||||
_categoriesViewModel = new CategoriesViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized CategoriesViewModel");
|
||||
_moreViewModel = new MoreViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized MoreViewModel");
|
||||
_analyticsViewModel = new AnalyticsViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
@@ -144,6 +161,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
IsDarkTheme = ThemeService.IsDarkTheme;
|
||||
|
||||
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
|
||||
AppData.StartRealtimeSync();
|
||||
CurrentView = _dashboardViewModel;
|
||||
IsDimmed = false;
|
||||
}
|
||||
@@ -153,6 +171,24 @@ public partial class MainViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shows a themed message box overlay. Safe to call from any child ViewModel.</summary>
|
||||
public void ShowMessage(MessageType type, string title, string message)
|
||||
{
|
||||
MessageBoxViewModel.Type = type;
|
||||
MessageBoxViewModel.Title = title;
|
||||
MessageBoxViewModel.Message = message;
|
||||
MessageBoxViewModel.OnClose = CloseMessageBox;
|
||||
IsMessageBoxVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseMessageBox()
|
||||
{
|
||||
IsMessageBoxVisible = false;
|
||||
IsDimmed = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenAddTransaction()
|
||||
{
|
||||
@@ -403,6 +439,18 @@ public partial class MainViewModel : ViewModelBase
|
||||
CurrentView = _budgetViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToCategories()
|
||||
{
|
||||
CurrentView = _categoriesViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToMore()
|
||||
{
|
||||
CurrentView = _moreViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToAnalytics()
|
||||
{
|
||||
@@ -432,10 +480,74 @@ public partial class MainViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns true if the back event was handled (suppress system back), false to let the system close the app.</summary>
|
||||
public bool HandleBackNavigation()
|
||||
{
|
||||
// 1. Close deepest-nested modal first (category form sits on top of transaction form)
|
||||
if (IsCategoryFormVisible)
|
||||
{
|
||||
CloseCategoryForm();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsTransactionFormVisible)
|
||||
{
|
||||
CloseTransactionForm();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsAccountFormVisible)
|
||||
{
|
||||
CloseAccountForm();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsBudgetFormVisible)
|
||||
{
|
||||
CloseBudgetForm();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsSavingsGoalDialogVisible)
|
||||
{
|
||||
CloseSavingsGoalDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Close dialogs inside AccountsView
|
||||
if (_accountsViewModel is { IsDeleteDialogVisible: true })
|
||||
{
|
||||
_accountsViewModel.IsDeleteDialogVisible = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_accountsViewModel is { IsArchiveDialogVisible: true })
|
||||
{
|
||||
_accountsViewModel.IsArchiveDialogVisible = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Close AccountsView bottom sheet
|
||||
if (_accountsViewModel?.TryCloseSheet?.Invoke() == true)
|
||||
return true;
|
||||
|
||||
// 4. Navigate back to dashboard from any non-dashboard main view
|
||||
if (!isOnDashboard)
|
||||
{
|
||||
CurrentView = _dashboardViewModel;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Already on dashboard — let the system handle (closes the app)
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool isOnDashboard => CurrentView is DashboardViewModel;
|
||||
public bool isOnTransactions => CurrentView is TransactionsViewModel;
|
||||
public bool isOnAccounts => CurrentView is AccountsViewModel;
|
||||
public bool isOnBudget => CurrentView is BudgetViewModel;
|
||||
public bool isOnCategories => CurrentView is CategoriesViewModel;
|
||||
public bool isOnAnalytics => CurrentView is AnalyticsViewModel;
|
||||
public bool isOnSettings => CurrentView is SettingsViewModel;
|
||||
public bool isOnMore => CurrentView is MoreViewModel or AnalyticsViewModel or BudgetViewModel or CategoriesViewModel;
|
||||
}
|
||||
32
Clario/ViewModels/MessageBoxViewModel.cs
Normal file
32
Clario/ViewModels/MessageBoxViewModel.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public enum MessageType { Error, Warning, Success, Info }
|
||||
|
||||
public partial class MessageBoxViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty] private MessageType _type = MessageType.Info;
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string _message = "";
|
||||
|
||||
public bool IsError => Type == MessageType.Error;
|
||||
public bool IsWarning => Type == MessageType.Warning;
|
||||
public bool IsSuccess => Type == MessageType.Success;
|
||||
public bool IsInfo => Type == MessageType.Info;
|
||||
|
||||
public Action? OnClose { get; set; }
|
||||
|
||||
partial void OnTypeChanged(MessageType value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsError));
|
||||
OnPropertyChanged(nameof(IsWarning));
|
||||
OnPropertyChanged(nameof(IsSuccess));
|
||||
OnPropertyChanged(nameof(IsInfo));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Close() => OnClose?.Invoke();
|
||||
}
|
||||
29
Clario/ViewModels/MoreViewModel.cs
Normal file
29
Clario/ViewModels/MoreViewModel.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class MoreViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToAnalytics()
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainVm)
|
||||
mainVm.GoToAnalyticsCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToBudget()
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainVm)
|
||||
mainVm.GoToBudgetCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToCategories()
|
||||
{
|
||||
if (parentViewModel is MainViewModel mainVm)
|
||||
mainVm.GoToCategoriesCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
67
Clario/ViewModels/ResetPasswordViewModel.cs
Normal file
67
Clario/ViewModels/ResetPasswordViewModel.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Supabase.Gotrue;
|
||||
using Supabase.Gotrue.Exceptions;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class ResetPasswordViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SetNewPasswordCommand))]
|
||||
private string _newPassword = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SetNewPasswordCommand))]
|
||||
private string _confirmPassword = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
[ObservableProperty] private bool _passwordUpdated;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSetPassword))]
|
||||
private async Task SetNewPassword()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.Auth.Update(new UserAttributes { Password = _newPassword });
|
||||
PasswordUpdated = true;
|
||||
}
|
||||
catch (GotrueException e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = e.Reason == FailureHint.Reason.UserBadPassword
|
||||
? "Password must be at least 6 characters."
|
||||
: "Something went wrong. Please try again.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = "Something went wrong. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToSignIn()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
desktop.MainWindow!.DataContext = new AuthViewModel();
|
||||
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime sv)
|
||||
sv.MainView!.DataContext = new AuthViewModel();
|
||||
}
|
||||
|
||||
private bool CanSetPassword =>
|
||||
!string.IsNullOrWhiteSpace(_newPassword) &&
|
||||
_newPassword.Length >= 6 &&
|
||||
_newPassword == _confirmPassword;
|
||||
}
|
||||
@@ -26,11 +26,11 @@ public partial class SetSavingsGoalDialogViewModel : ViewModelBase
|
||||
public bool IsValid =>
|
||||
decimal.TryParse(GoalInput, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) && v >= 0;
|
||||
|
||||
// ── Callbacks ────────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
|
||||
// ── Setup ────────────────────────────────────────────────
|
||||
// Setup
|
||||
public void Setup(decimal? currentGoal)
|
||||
{
|
||||
GoalInput = currentGoal.HasValue
|
||||
@@ -39,7 +39,7 @@ public partial class SetSavingsGoalDialogViewModel : ViewModelBase
|
||||
ErrorMessage = null;
|
||||
}
|
||||
|
||||
// ── Commands ─────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => OnCancelled?.Invoke();
|
||||
|
||||
@@ -24,29 +24,29 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
public static readonly HttpClient _HttpClient = new();
|
||||
|
||||
|
||||
// ── Profile fields ───────────────────────────────────────
|
||||
// Profile fields
|
||||
[ObservableProperty] private string _displayName = "";
|
||||
[ObservableProperty] private string _avatarUrl = "";
|
||||
[ObservableProperty] private Bitmap? _avatarImage;
|
||||
[ObservableProperty] private string _selectedTheme = "system";
|
||||
[ObservableProperty] private string _selectedLanguage = "en";
|
||||
|
||||
// ── Account (auth) fields ────────────────────────────────
|
||||
// Account (auth) fields
|
||||
[ObservableProperty] private string _maskedEmail = "";
|
||||
private string _fullEmail = "";
|
||||
|
||||
// ── Change email flow ────────────────────────────────────
|
||||
// Change email flow
|
||||
[ObservableProperty] private bool _isChangingEmail = false;
|
||||
[ObservableProperty] private string _newEmail = "";
|
||||
[ObservableProperty] private string _emailConfirmPassword = "";
|
||||
|
||||
// ── Change password flow ─────────────────────────────────
|
||||
// Change password flow
|
||||
[ObservableProperty] private bool _isChangingPassword = false;
|
||||
[ObservableProperty] private string _currentPassword = "";
|
||||
[ObservableProperty] private string _newPassword = "";
|
||||
[ObservableProperty] private string _confirmNewPassword = "";
|
||||
|
||||
// ── UI state ─────────────────────────────────────────────
|
||||
// UI state
|
||||
[ObservableProperty] private bool _isSaving = false;
|
||||
[ObservableProperty] private bool _isUploadingAvatar = false;
|
||||
|
||||
@@ -76,7 +76,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
public bool HasPasswordError => !string.IsNullOrEmpty(PasswordErrorMessage);
|
||||
public bool HasAvatar => !string.IsNullOrEmpty(AvatarUrl);
|
||||
|
||||
// ── Options ──────────────────────────────────────────────
|
||||
// Options
|
||||
|
||||
public ObservableCollection<(string Value, string Label)> Themes { get; } = new()
|
||||
{
|
||||
@@ -118,7 +118,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
SelectedLanguage = value switch { 0 => "en", 1 => "ar", _ => "en" };
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────
|
||||
// Init
|
||||
public SettingsViewModel()
|
||||
{
|
||||
_ = Initialize();
|
||||
@@ -155,7 +155,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
return $"{visible}{masked}{domain}";
|
||||
}
|
||||
|
||||
// ── Avatar commands ───────────────────────────────────────
|
||||
// Avatar commands
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UploadAvatar()
|
||||
@@ -215,7 +215,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save profile ─────────────────────────────────────────
|
||||
// Save profile
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveProfile()
|
||||
@@ -263,7 +263,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Change email ─────────────────────────────────────────
|
||||
// Change email
|
||||
|
||||
[RelayCommand]
|
||||
private void StartChangeEmail()
|
||||
@@ -327,7 +327,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Change password ──────────────────────────────────────
|
||||
// Change password
|
||||
|
||||
[RelayCommand]
|
||||
private void StartChangePassword()
|
||||
@@ -397,7 +397,7 @@ public partial class SettingsViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sign out ─────────────────────────────────────────────
|
||||
// Sign out
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SignOut()
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
// ── Mode ────────────────────────────────────────────────
|
||||
// Mode
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
@@ -25,7 +25,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : (IsTransfer ? "Save Transfer" : "Save Transaction");
|
||||
|
||||
// ── Fields ──────────────────────────────────────────────
|
||||
// Fields
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome), nameof(IsTransfer), nameof(IsValid), nameof(FormTitle), nameof(SaveButtonLabel))]
|
||||
private string _type = "expense";
|
||||
@@ -64,7 +64,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
[ObservableProperty] private ObservableCollection<Category> _categories = new();
|
||||
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
|
||||
|
||||
// ── Validation ──────────────────────────────────────────
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
@@ -80,7 +80,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
? SelectedAccount is not null && SelectedToAccount is not null && SelectedAccount.Id != SelectedToAccount.Id
|
||||
: !string.IsNullOrWhiteSpace(Description) && SelectedCategory is not null && SelectedAccount is not null);
|
||||
|
||||
// ── Callbacks ───────────────────────────────────────────
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
public Action? OnDeleted;
|
||||
@@ -89,17 +89,17 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private bool _showDeleteConfirm = false;
|
||||
|
||||
// ── Edit mode: original transaction ─────────────────────
|
||||
// Edit mode: original transaction
|
||||
private Transaction? _editingTransaction;
|
||||
private Guid? _editingId;
|
||||
private Guid? _transferPairId;
|
||||
private decimal _editingOriginalAmount;
|
||||
private Guid? _editingOriginalCategoryId;
|
||||
|
||||
// ── Result transaction ──────────────────────────────────
|
||||
// Result transaction
|
||||
public Transaction? ResultTransaction { get; set; }
|
||||
|
||||
// ── Budget warning ──────────────────────────────────────
|
||||
// Budget warning
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasBudgetWarning), nameof(HasBudgetApproachingWarning))]
|
||||
private string? _budgetWarningMessage;
|
||||
|
||||
@@ -109,7 +109,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
public bool HasBudgetWarning => !string.IsNullOrEmpty(BudgetWarningMessage);
|
||||
public bool HasBudgetApproachingWarning => HasBudgetWarning && !BudgetWarningIsOverBudget;
|
||||
|
||||
// ── Commands ────────────────────────────────────────────
|
||||
// Commands
|
||||
|
||||
partial void OnSelectedCategoryChanged(Category? value)
|
||||
{
|
||||
@@ -414,7 +414,7 @@ public partial class TransactionFormViewModel : ViewModelBase
|
||||
OnCancelled?.Invoke();
|
||||
}
|
||||
|
||||
// ── Public setup methods ─────────────────────────────────
|
||||
// Public setup methods
|
||||
|
||||
/// <summary>Call this to open the form for adding a new transaction.</summary>
|
||||
public void SetupForAdd()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Clario.Data;
|
||||
using Clario.Messages;
|
||||
@@ -11,60 +10,98 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
// ReSharper disable PossibleMultipleEnumeration
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class TransactionsViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
private GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
// ── Filter dropdowns ────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Category> _categories = new();
|
||||
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilteredTransactionCount))]
|
||||
private static readonly IReadOnlyList<string> _sortOptions = new[]
|
||||
{
|
||||
"Date — Newest first", "Date — Oldest first",
|
||||
"Amount — High to low", "Amount — Low to high",
|
||||
"Category A → Z"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<string> _dateRangeOptions = new[]
|
||||
{
|
||||
"All Time", "Today", "This Week", "This Month",
|
||||
"Last Month", "This Quarter", "This Year", "Custom Range"
|
||||
};
|
||||
|
||||
public IReadOnlyList<string> SortOptions => _sortOptions;
|
||||
public IReadOnlyList<string> DateRangeOptions => _dateRangeOptions;
|
||||
|
||||
// ── Active filter values ─────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private Category _selectedCategory;
|
||||
[ObservableProperty] private Account _selectedAccount;
|
||||
[ObservableProperty] private string _selectedSortOption = _sortOptions[0];
|
||||
[ObservableProperty] private string _selectedDateRangeOption = _dateRangeOptions[0];
|
||||
|
||||
[ObservableProperty] private List<DateTime>? _selectedDates = new()
|
||||
{
|
||||
new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1),
|
||||
new DateTime(DateTime.Now.Year, DateTime.Now.Month,
|
||||
DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month))
|
||||
};
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome),
|
||||
nameof(FilterTypeExpense), nameof(FilterTypeTransfer))]
|
||||
private string _transactionType = "all";
|
||||
|
||||
public bool FilterTypeAll => TransactionType == "all";
|
||||
public bool FilterTypeIncome => TransactionType == "income";
|
||||
public bool FilterTypeExpense => TransactionType == "expense";
|
||||
public bool FilterTypeTransfer => TransactionType == "transfer";
|
||||
|
||||
// ── Filtered / paged data ────────────────────────────────────────────────
|
||||
|
||||
private List<Transaction> _filteredAll = new();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FilteredTransactionCount))]
|
||||
private List<Transaction> _filteredTransactions = new();
|
||||
|
||||
public int FilteredTransactionCount => _filteredTransactions.Count;
|
||||
[ObservableProperty] private ObservableCollection<Transaction> _pagedTransactions = new();
|
||||
|
||||
// ── Desktop pagination ───────────────────────────────────────────────────
|
||||
|
||||
private int _pageSize = 25;
|
||||
[ObservableProperty] private int _pageSizeIndex;
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(TotalPages))] [NotifyCanExecuteChangedFor(nameof(NextPageCommand), nameof(PreviousPageCommand))]
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(TotalPages))]
|
||||
[NotifyCanExecuteChangedFor(nameof(NextPageCommand), nameof(PreviousPageCommand))]
|
||||
private int _currentPage = 1;
|
||||
|
||||
[ObservableProperty] private string _paginationSummaryText;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<Transaction> _pagedTransactions = new();
|
||||
|
||||
[ObservableProperty] private ObservableCollection<string> _sortOptions = new()
|
||||
{
|
||||
"Date — Newest first",
|
||||
"Date — Oldest first",
|
||||
"Amount — High to low",
|
||||
"Amount — Low to high",
|
||||
"Category A → Z"
|
||||
};
|
||||
|
||||
[ObservableProperty] private ObservableCollection<string> _DateRangeOptions = new()
|
||||
{
|
||||
"All Time",
|
||||
"Today",
|
||||
"This Week",
|
||||
"This Month",
|
||||
"Last Month",
|
||||
"This Quarter",
|
||||
"This Year",
|
||||
"Custom Range"
|
||||
};
|
||||
|
||||
public List<int> PageNumbers { get; set; }
|
||||
[ObservableProperty] private string _paginationSummaryText = "";
|
||||
[ObservableProperty] private ObservableCollection<int> _visiblePageNumbers = new();
|
||||
public int TotalPages => (int)Math.Ceiling(FilteredTransactions.Count / (double)_pageSize);
|
||||
public bool HasNoTransactions => FilteredTransactions.Count == 0;
|
||||
public bool HasNextPage => CurrentPage < TotalPages;
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
|
||||
public int TotalPages => (int)Math.Ceiling(_filteredAll.Count / (double)_pageSize);
|
||||
public bool HasNoTransactions => _filteredAll.Count == 0;
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
|
||||
// HasNextPage differs by platform
|
||||
public bool HasNextPage => App.IsMobile
|
||||
? _mobileDisplayCount < _filteredAll.Count
|
||||
: CurrentPage < TotalPages;
|
||||
|
||||
// ── Mobile infinite scroll ───────────────────────────────────────────────
|
||||
|
||||
/// How many real (non-header) items are currently rendered in PagedTransactions.
|
||||
private int _mobileDisplayCount;
|
||||
|
||||
// ── Summary stats ────────────────────────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private double _totalExpenses;
|
||||
[ObservableProperty] private double _totalIncome;
|
||||
@@ -75,261 +112,23 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
public string PrimaryCurrencySymbol =>
|
||||
CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
|
||||
[ObservableProperty] private string _searchText = "";
|
||||
[ObservableProperty] private Category _selectedCategory;
|
||||
[ObservableProperty] private Account _selectedAccount;
|
||||
[ObservableProperty] private string _selectedSortOption;
|
||||
[ObservableProperty] private string _selectedDateRangeOption;
|
||||
|
||||
[ObservableProperty] private List<DateTime>? _selectedDates = new()
|
||||
{
|
||||
new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1),
|
||||
new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month))
|
||||
};
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FilterTypeAll), nameof(FilterTypeIncome), nameof(FilterTypeExpense), nameof(FilterTypeTransfer))]
|
||||
private string _transactionType = "all";
|
||||
|
||||
// ── Constructor ──────────────────────────────────────────────────────────
|
||||
|
||||
public TransactionsViewModel()
|
||||
{
|
||||
AppData.Transactions.CollectionChanged += (_, _) =>
|
||||
Track(AppData.Transactions, (_, _) =>
|
||||
{
|
||||
InitializeCategories();
|
||||
InitializeAccounts();
|
||||
LoadPage(1);
|
||||
};
|
||||
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => LoadPage(CurrentPage));
|
||||
Refresh();
|
||||
});
|
||||
|
||||
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => Refresh());
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
partial void OnPageSizeIndexChanged(int value)
|
||||
{
|
||||
_pageSize = value switch
|
||||
{
|
||||
0 => 25,
|
||||
1 => 50,
|
||||
2 => 100,
|
||||
_ => 25
|
||||
};
|
||||
|
||||
|
||||
LoadPage(1);
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
OnPropertyChanged(nameof(HasPreviousPage));
|
||||
}
|
||||
|
||||
|
||||
partial void OnCurrentPageChanged(int value)
|
||||
{
|
||||
LoadPage(value);
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
OnPropertyChanged(nameof(HasPreviousPage));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadPageStr(string page)
|
||||
{
|
||||
LoadPage(int.Parse(page));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadPage(int page)
|
||||
{
|
||||
ApplyFilters();
|
||||
if (CurrentPage != page) CurrentPage = page;
|
||||
var items = FilteredTransactions.Skip((page - 1) * _pageSize)
|
||||
.Take(_pageSize);
|
||||
|
||||
OnPropertyChanged(nameof(HasNoTransactions));
|
||||
PagedTransactions.Clear();
|
||||
foreach (var item in items)
|
||||
PagedTransactions.Add(item);
|
||||
PaginationSummaryText =
|
||||
$"Showing {((page - 1) * _pageSize) + 1}-{(Math.Min(page * _pageSize, FilteredTransactions.Count))} of {FilteredTransactions.Count} transactions";
|
||||
PageNumbers = Enumerable.Range(1, Math.Min(TotalPages, 5)).ToList();
|
||||
var numbers = GetSurrounding(PageNumbers, page);
|
||||
VisiblePageNumbers.Clear();
|
||||
foreach (var number in numbers)
|
||||
VisiblePageNumbers.Add(number);
|
||||
WeakReferenceMessenger.Default.Send(new TransactionsScrollToTop());
|
||||
GroupTransactions();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ApplyFilters()
|
||||
{
|
||||
var filtered = AppData.Transactions.Where(x =>
|
||||
x.Type != "transfer_in" &&
|
||||
(x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| x.Note!.Contains(SearchText, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
||||
switch (SelectedDateRangeOption)
|
||||
{
|
||||
case "All Time":
|
||||
DateRangeLabel = "ALL TIME";
|
||||
break;
|
||||
case "Today":
|
||||
filtered = filtered.Where(x => x.Date == DateTime.Now.Date);
|
||||
DateRangeLabel = DateTime.Now.ToString("MMM d, yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "This Week":
|
||||
var startOfWeek = DateTime.Now.Date.AddDays(-(int)DateTime.Now.DayOfWeek);
|
||||
var endOfWeek = startOfWeek.AddDays(6);
|
||||
filtered = filtered.Where(x => x.Date.Date >= startOfWeek && x.Date.Date <= endOfWeek);
|
||||
DateRangeLabel = "THIS WEEK";
|
||||
break;
|
||||
case "This Month":
|
||||
filtered = filtered.Where(x => x.Date.Month == DateTime.Now.Month);
|
||||
DateRangeLabel = DateTime.Now.ToString("MMMM yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "Last Month":
|
||||
var lastMonth = DateTime.Now.AddMonths(-1);
|
||||
filtered = filtered.Where(x => x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year);
|
||||
DateRangeLabel = lastMonth.ToString("MMMM yyyy", culture).ToUpper();
|
||||
break;
|
||||
case "This Quarter":
|
||||
var startOfQuarter = DateTime.Now.AddMonths(-(DateTime.Now.Month - 1) % 3);
|
||||
var endOfQuarter = startOfQuarter.AddMonths(3);
|
||||
filtered = filtered.Where(x => x.Date >= startOfQuarter && x.Date <= endOfQuarter);
|
||||
DateRangeLabel = $"Q{(DateTime.Now.Month - 1) / 3 + 1} {DateTime.Now.Year}";
|
||||
break;
|
||||
case "This Year":
|
||||
filtered = filtered.Where(x => x.Date.Year == DateTime.Now.Year);
|
||||
DateRangeLabel = DateTime.Now.Year.ToString();
|
||||
break;
|
||||
case "Custom Range":
|
||||
if (SelectedDates is not null && SelectedDates.Count > 0)
|
||||
{
|
||||
var ordered = SelectedDates
|
||||
.Select(d => d.Date)
|
||||
.Distinct()
|
||||
.OrderBy(d => d)
|
||||
.ToList();
|
||||
|
||||
var start = ordered.First();
|
||||
var end = ordered.Last();
|
||||
|
||||
if (SelectedDates.Count == 1)
|
||||
{
|
||||
filtered = filtered.Where(x => x.Date.Date == start);
|
||||
DateRangeLabel = start.ToString("MMM dd, yyyy", culture).ToUpper();
|
||||
}
|
||||
else
|
||||
{
|
||||
filtered = filtered.Where(x => x.Date.Date >= start && x.Date.Date <= end);
|
||||
DateRangeLabel = $"{start.ToString("MMM dd", culture)} - {end.ToString("MMM dd, yyyy", culture)}".ToUpper();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Calculate totals based on date-filtered transactions (transfers excluded)
|
||||
TotalExpenses = filtered.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
TotalIncome = filtered.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
|
||||
if (SelectedCategory.Name != "All Categories")
|
||||
filtered = filtered.Where(x => x.CategoryId == SelectedCategory.Id);
|
||||
|
||||
if (SelectedAccount.Name != "All Accounts")
|
||||
filtered = filtered.Where(x => x.AccountId == SelectedAccount.Id);
|
||||
|
||||
if (TransactionType == "income")
|
||||
filtered = filtered.Where(x => x.Type == "income");
|
||||
else if (TransactionType == "expense")
|
||||
filtered = filtered.Where(x => x.Type == "expense");
|
||||
else if (TransactionType == "transfer")
|
||||
filtered = filtered.Where(x => x.IsTransfer);
|
||||
|
||||
switch (SelectedSortOption)
|
||||
{
|
||||
case "Date — Newest first":
|
||||
filtered = filtered.OrderByDescending(x => x.Date);
|
||||
break;
|
||||
case "Date — Oldest first":
|
||||
filtered = filtered.OrderBy(x => x.Date);
|
||||
break;
|
||||
case "Amount — High to low":
|
||||
filtered = filtered.OrderByDescending(x => x.Amount);
|
||||
break;
|
||||
case "Amount — Low to high":
|
||||
filtered = filtered.OrderBy(x => x.Amount);
|
||||
break;
|
||||
case "Category A → Z":
|
||||
filtered = filtered.OrderBy(x => x.Category?.Name);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
FilteredTransactions = filtered.ToList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetFilters()
|
||||
{
|
||||
SearchText = "";
|
||||
SelectedCategory = Categories.First();
|
||||
SelectedAccount = Accounts.First();
|
||||
TransactionType = "all";
|
||||
SelectedSortOption = SortOptions.First();
|
||||
SelectedDateRangeOption = DateRangeOptions.First();
|
||||
LoadPage(1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SetTransactionType(string type)
|
||||
{
|
||||
TransactionType = type;
|
||||
}
|
||||
|
||||
public bool FilterTypeAll => TransactionType == "all";
|
||||
public bool FilterTypeIncome => TransactionType == "income";
|
||||
public bool FilterTypeExpense => TransactionType == "expense";
|
||||
public bool FilterTypeTransfer => TransactionType == "transfer";
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasNextPage))]
|
||||
private void NextPage()
|
||||
{
|
||||
if (CurrentPage < TotalPages) CurrentPage++;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasPreviousPage))]
|
||||
private void PreviousPage()
|
||||
{
|
||||
if (CurrentPage > 1) CurrentPage--;
|
||||
}
|
||||
|
||||
private void GroupTransactions()
|
||||
{
|
||||
var ToRemove = PagedTransactions.Where(x => x.GroupHeader).ToList();
|
||||
foreach (var item in ToRemove)
|
||||
{
|
||||
PagedTransactions.Remove(item);
|
||||
}
|
||||
|
||||
var dates = PagedTransactions
|
||||
.Where(x => !x.GroupHeader)
|
||||
.Select(x => x.Date.Date) // strip time
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var date in dates)
|
||||
{
|
||||
var index = PagedTransactions.IndexOf(PagedTransactions.First(x => x.Date.Date == date && !x.GroupHeader));
|
||||
string label;
|
||||
var culture = new CultureInfo("en-US");
|
||||
if (date.Date == DateTime.Now.Date) label = "Today - " + date.ToString("MMM dd", culture);
|
||||
else if (date.Date == DateTime.Now.AddDays(-1).Date) label = "Yesterday - " + date.ToString("MMM dd", culture);
|
||||
else label = date.ToString("MMM dd, yyyy", culture);
|
||||
var header = new Transaction { Description = label, Date = date, GroupHeader = true };
|
||||
|
||||
PagedTransactions.Insert(index, header);
|
||||
}
|
||||
}
|
||||
// ── Initialization ───────────────────────────────────────────────────────
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
@@ -337,9 +136,7 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
{
|
||||
InitializeCategories();
|
||||
InitializeAccounts();
|
||||
|
||||
CalculateMonthlyFinancials();
|
||||
|
||||
CurrentPage = 1;
|
||||
OnPropertyChanged(nameof(TotalPages));
|
||||
ResetFilters();
|
||||
@@ -354,59 +151,274 @@ public partial class TransactionsViewModel : ViewModelBase
|
||||
private void InitializeCategories()
|
||||
{
|
||||
Categories.Clear();
|
||||
Categories.Insert(0, new Category() { Name = "All Categories" });
|
||||
foreach (var appDataCategory in AppData.Categories)
|
||||
{
|
||||
Categories.Add(appDataCategory);
|
||||
}
|
||||
|
||||
Categories.Insert(0, new Category { Name = "All Categories" });
|
||||
foreach (var cat in AppData.Categories)
|
||||
Categories.Add(cat);
|
||||
SelectedCategory = Categories.First();
|
||||
}
|
||||
|
||||
private void InitializeAccounts()
|
||||
{
|
||||
Accounts.Clear();
|
||||
Accounts.Insert(0, new Account() { Name = "All Accounts" });
|
||||
foreach (var appDataAccount in AppData.Accounts)
|
||||
{
|
||||
Accounts.Add(appDataAccount);
|
||||
}
|
||||
|
||||
Accounts.Insert(0, new Account { Name = "All Accounts" });
|
||||
foreach (var acc in AppData.Accounts)
|
||||
Accounts.Add(acc);
|
||||
SelectedAccount = Accounts.First();
|
||||
}
|
||||
|
||||
private void CalculateMonthlyFinancials()
|
||||
{
|
||||
TotalExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
TotalIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month).Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
ExpensesCount = AppData.Transactions.Count(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month);
|
||||
IncomeCount = AppData.Transactions.Count(x => x.Type == "income" && x.Date.Month == DateTime.Now.Month);
|
||||
var now = DateTime.Now;
|
||||
var monthly = AppData.Transactions
|
||||
.Where(x => x.Date.Month == now.Month && x.Date.Year == now.Year);
|
||||
TotalExpenses = monthly.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
TotalIncome = monthly.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
ExpensesCount = monthly.Count(x => x.Type == "expense");
|
||||
IncomeCount = monthly.Count(x => x.Type == "income");
|
||||
}
|
||||
|
||||
public static List<T> GetSurrounding<T>(List<T> list, T item, int count = 5)
|
||||
// ── Filter pipeline ──────────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void ApplyFilters()
|
||||
{
|
||||
var filteringByAccount = SelectedAccount?.Name != "All Accounts";
|
||||
|
||||
// 1. Search + transfer-in visibility
|
||||
IEnumerable<Transaction> source = AppData.Transactions.Where(x =>
|
||||
(filteringByAccount || x.Type != "transfer_in") &&
|
||||
(x.Description.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| (x.Note?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false)));
|
||||
|
||||
// 2. Date range
|
||||
source = ApplyDateFilter(source, out var label);
|
||||
DateRangeLabel = label;
|
||||
|
||||
// 3. Totals use the date-scoped set (before category/type filters)
|
||||
CalculateTotalsFromSource(source);
|
||||
|
||||
// 4. Remaining filters
|
||||
source = ApplyCategoryFilter(source);
|
||||
source = ApplyAccountFilter(source);
|
||||
source = ApplyTypeFilter(source);
|
||||
source = ApplySortFilter(source);
|
||||
|
||||
_filteredAll = source.ToList();
|
||||
FilteredTransactions = _filteredAll;
|
||||
}
|
||||
|
||||
private IEnumerable<Transaction> ApplyDateFilter(IEnumerable<Transaction> source, out string label)
|
||||
{
|
||||
var (start, end, lbl) = DateRangeService.Resolve(SelectedDateRangeOption, SelectedDates);
|
||||
label = lbl;
|
||||
if (start is null || end is null) return source;
|
||||
return source.Where(x => x.Date.Date >= start.Value && x.Date.Date <= end.Value);
|
||||
}
|
||||
|
||||
private void CalculateTotalsFromSource(IEnumerable<Transaction> source)
|
||||
{
|
||||
var list = source.ToList();
|
||||
TotalExpenses = list.Where(x => x.Type == "expense").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
TotalIncome = list.Where(x => x.Type == "income").Sum(x => Convert.ToDouble(x.ConvertedAmount));
|
||||
}
|
||||
|
||||
private IEnumerable<Transaction> ApplyCategoryFilter(IEnumerable<Transaction> source)
|
||||
{
|
||||
if (SelectedCategory?.Name == "All Categories") return source;
|
||||
return source.Where(x => x.CategoryId == SelectedCategory?.Id);
|
||||
}
|
||||
|
||||
private IEnumerable<Transaction> ApplyAccountFilter(IEnumerable<Transaction> source)
|
||||
{
|
||||
if (SelectedAccount?.Name == "All Accounts") return source;
|
||||
return source.Where(x => x.AccountId == SelectedAccount?.Id);
|
||||
}
|
||||
|
||||
private IEnumerable<Transaction> ApplyTypeFilter(IEnumerable<Transaction> source) =>
|
||||
TransactionType switch
|
||||
{
|
||||
"income" => source.Where(x => x.Type == "income"),
|
||||
"expense" => source.Where(x => x.Type == "expense"),
|
||||
"transfer" => source.Where(x => x.IsTransfer),
|
||||
_ => source
|
||||
};
|
||||
|
||||
private IEnumerable<Transaction> ApplySortFilter(IEnumerable<Transaction> source) =>
|
||||
SelectedSortOption switch
|
||||
{
|
||||
"Date — Oldest first" => source.OrderBy(x => x.Date),
|
||||
"Amount — High to low" => source.OrderByDescending(x => x.Amount),
|
||||
"Amount — Low to high" => source.OrderBy(x => x.Amount),
|
||||
"Category A → Z" => source.OrderBy(x => x.Category?.Name),
|
||||
_ => source.OrderByDescending(x => x.Date) // default: newest first
|
||||
};
|
||||
|
||||
// ── Desktop pagination ───────────────────────────────────────────────────
|
||||
|
||||
partial void OnPageSizeIndexChanged(int value)
|
||||
{
|
||||
_pageSize = value switch { 1 => 50, 2 => 100, _ => 25 };
|
||||
LoadPage(1);
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
OnPropertyChanged(nameof(HasPreviousPage));
|
||||
}
|
||||
|
||||
partial void OnCurrentPageChanged(int value)
|
||||
{
|
||||
if (App.IsMobile) return;
|
||||
LoadPage(value);
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
OnPropertyChanged(nameof(HasPreviousPage));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadPageStr(string page) => LoadPage(int.Parse(page));
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadPage(int page)
|
||||
{
|
||||
ApplyFilters();
|
||||
if (CurrentPage != page) CurrentPage = page;
|
||||
|
||||
var items = _filteredAll.Skip((page - 1) * _pageSize).Take(_pageSize).ToList();
|
||||
|
||||
PagedTransactions.Clear();
|
||||
foreach (var item in items) PagedTransactions.Add(item);
|
||||
|
||||
OnPropertyChanged(nameof(HasNoTransactions));
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
OnPropertyChanged(nameof(HasPreviousPage));
|
||||
|
||||
PaginationSummaryText = _filteredAll.Count == 0
|
||||
? "No transactions"
|
||||
: $"Showing {(page - 1) * _pageSize + 1}–{Math.Min(page * _pageSize, _filteredAll.Count)} of {_filteredAll.Count}";
|
||||
|
||||
var allPages = Enumerable.Range(1, Math.Max(TotalPages, 1)).ToList();
|
||||
VisiblePageNumbers.Clear();
|
||||
foreach (var n in GetSurrounding(allPages, page)) VisiblePageNumbers.Add(n);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new TransactionsScrollToTop());
|
||||
GroupTransactions();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasNextPage))]
|
||||
private void NextPage() { if (CurrentPage < TotalPages) CurrentPage++; }
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasPreviousPage))]
|
||||
private void PreviousPage() { if (CurrentPage > 1) CurrentPage--; }
|
||||
|
||||
// ── Mobile infinite scroll ───────────────────────────────────────────────
|
||||
|
||||
private void RefreshMobile()
|
||||
{
|
||||
_mobileDisplayCount = 0;
|
||||
PagedTransactions.Clear();
|
||||
AppendMobileItems(_pageSize * 3);
|
||||
OnPropertyChanged(nameof(HasNoTransactions));
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
}
|
||||
|
||||
private void AppendMobileItems(int count)
|
||||
{
|
||||
var batch = _filteredAll.Skip(_mobileDisplayCount).Take(count).ToList();
|
||||
|
||||
foreach (var item in batch)
|
||||
{
|
||||
var needsHeader = _mobileDisplayCount == 0
|
||||
|| item.Date.Date != _filteredAll[_mobileDisplayCount - 1].Date.Date;
|
||||
|
||||
if (needsHeader)
|
||||
{
|
||||
PagedTransactions.Add(new Transaction
|
||||
{
|
||||
Description = DateRangeService.FormatGroupHeader(item.Date),
|
||||
Date = item.Date,
|
||||
GroupHeader = true
|
||||
});
|
||||
}
|
||||
|
||||
PagedTransactions.Add(item);
|
||||
_mobileDisplayCount++;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasNextPage));
|
||||
}
|
||||
|
||||
/// Adds 3 pages of items at once. Shown behind "Load More" button.
|
||||
[RelayCommand(CanExecute = nameof(HasNextPage))]
|
||||
private void LoadMore()
|
||||
{
|
||||
if (_mobileDisplayCount >= _filteredAll.Count) return;
|
||||
AppendMobileItems(_pageSize * 3);
|
||||
}
|
||||
|
||||
// ── Shared helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
CalculateMonthlyFinancials();
|
||||
if (App.IsMobile) { ApplyFilters(); RefreshMobile(); }
|
||||
else LoadPage(CurrentPage);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetFilters()
|
||||
{
|
||||
SearchText = "";
|
||||
SelectedCategory = Categories.FirstOrDefault() ?? new Category { Name = "All Categories" };
|
||||
SelectedAccount = Accounts.FirstOrDefault() ?? new Account { Name = "All Accounts" };
|
||||
TransactionType = "all";
|
||||
SelectedSortOption = SortOptions[0];
|
||||
SelectedDateRangeOption = DateRangeOptions[0];
|
||||
|
||||
if (App.IsMobile) { ApplyFilters(); RefreshMobile(); }
|
||||
else LoadPage(1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SetTransactionType(string type) => TransactionType = type;
|
||||
|
||||
/// Desktop: inserts date group headers into PagedTransactions.
|
||||
private void GroupTransactions()
|
||||
{
|
||||
// Remove all existing headers
|
||||
foreach (var h in PagedTransactions.Where(x => x.GroupHeader).ToList())
|
||||
PagedTransactions.Remove(h);
|
||||
|
||||
// Insert a header before the first item of each date group
|
||||
var dates = PagedTransactions.Select(x => x.Date.Date).Distinct().ToList();
|
||||
foreach (var date in dates)
|
||||
{
|
||||
var firstItem = PagedTransactions.FirstOrDefault(x => !x.GroupHeader && x.Date.Date == date);
|
||||
if (firstItem is null) continue;
|
||||
PagedTransactions.Insert(PagedTransactions.IndexOf(firstItem), new Transaction
|
||||
{
|
||||
Description = DateRangeService.FormatGroupHeader(date),
|
||||
Date = date,
|
||||
GroupHeader = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static List<T> GetSurrounding<T>(List<T> list, T item, int count = 5)
|
||||
{
|
||||
var index = list.IndexOf(item);
|
||||
if (index == -1) return new List<T>();
|
||||
|
||||
var half = count / 2;
|
||||
var start = Math.Max(0, index - half);
|
||||
var end = Math.Min(list.Count, start + count);
|
||||
|
||||
// shift start back if end hit the boundary
|
||||
start = Math.Max(0, end - count);
|
||||
|
||||
return list.GetRange(start, end - start);
|
||||
var start = Math.Max(0, Math.Min(index - count / 2, list.Count - count));
|
||||
return list.GetRange(start, Math.Min(count, list.Count - start));
|
||||
}
|
||||
|
||||
// ── Navigation ───────────────────────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateTransaction()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddTransaction();
|
||||
if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EditTransaction(Transaction transaction)
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenEditTransaction(transaction);
|
||||
if (parentViewModel is MainViewModel main) main.OpenEditTransaction(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
public abstract class ViewModelBase : ObservableObject, IDisposable
|
||||
{
|
||||
}
|
||||
private readonly System.Collections.Generic.List<Action> _cleanup = new();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a CollectionChanged event and registers automatic unsubscription on Dispose.
|
||||
/// </summary>
|
||||
protected void Track(INotifyCollectionChanged collection, NotifyCollectionChangedEventHandler handler)
|
||||
{
|
||||
collection.CollectionChanged += handler;
|
||||
_cleanup.Add(() => collection.CollectionChanged -= handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an arbitrary cleanup action to run on Dispose.
|
||||
/// </summary>
|
||||
protected void OnDispose(Action action) => _cleanup.Add(action);
|
||||
|
||||
protected virtual void DisposeManaged() { }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeManaged();
|
||||
foreach (var action in _cleanup) action();
|
||||
_cleanup.Clear();
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
<vm:AccountFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<!-- Dim overlay -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<!-- Modal card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -29,7 +29,7 @@
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
@@ -66,7 +66,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Name ──────────────────────── -->
|
||||
<!-- Name -->
|
||||
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Name, Mode=TwoWay}"
|
||||
Watermark="e.g. Main Checking"
|
||||
@@ -76,7 +76,7 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- ── Type ─────────────────────────── -->
|
||||
<!-- Type -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -92,7 +92,7 @@
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Institution + Mask ──────────── -->
|
||||
<!-- Institution + Mask -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Institution -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
@@ -122,7 +122,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Opening Balance + Currency ──────────── -->
|
||||
<!-- Opening Balance + Currency -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Opening Balance -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
@@ -204,7 +204,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Credit Limit (if type is credit) ──────────── -->
|
||||
<!-- Credit Limit (if type is credit) -->
|
||||
<StackPanel Spacing="6" Margin="0,0,0,16" IsVisible="{Binding IsCredit}">
|
||||
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
@@ -236,7 +236,7 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Opened At ──────────────────────── -->
|
||||
<!-- Opened At -->
|
||||
<TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -250,7 +250,7 @@
|
||||
Padding="12,10" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Icon + Color ──────────── -->
|
||||
<!-- Icon + Color -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Icon -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
@@ -304,7 +304,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Primary account toggle ──────────── -->
|
||||
<!-- Primary account toggle -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="PRIMARY ACCOUNT" Classes="label" />
|
||||
@@ -320,7 +320,7 @@
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -339,7 +339,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<vm:AccountsViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
|
||||
<!-- TOP BAR -->
|
||||
<!-- TOP BAR -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,24">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding ActiveAccountCount, StringFormat='{}{0} accounts'}" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
@@ -35,9 +35,9 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- MAIN CONTENT Left * — account cards list Right 340 — selected account detail panel -->
|
||||
<!-- MAIN CONTENT Left * — account cards list Right 340 — selected account detail panel -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto">
|
||||
<!-- LEFT — Account Cards -->
|
||||
<!-- LEFT — Account Cards -->
|
||||
<Grid Grid.Column="0" RowDefinitions="*,Auto">
|
||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="0,0,20,0" Padding="8 0">
|
||||
<StackPanel Spacing="12" Margin="0 0 0 28">
|
||||
@@ -179,7 +179,7 @@
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Width="340" Padding="8 0"
|
||||
IsVisible="{Binding SelectedAccount, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Spacing="14" Margin="0 0 0 28">
|
||||
<!-- Account detail card -->
|
||||
<!-- Account detail card -->
|
||||
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
|
||||
Padding="22">
|
||||
<StackPanel Spacing="18">
|
||||
@@ -263,7 +263,7 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Monthly Flow -->
|
||||
<!-- Monthly Flow -->
|
||||
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
|
||||
Padding="22">
|
||||
<StackPanel Spacing="14">
|
||||
@@ -308,7 +308,7 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Recent Transactions -->
|
||||
<!-- Recent Transactions -->
|
||||
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
|
||||
Padding="22">
|
||||
<StackPanel Spacing="14">
|
||||
@@ -375,7 +375,7 @@
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Net Worth Contribution -->
|
||||
<!-- Net Worth Contribution -->
|
||||
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
|
||||
Padding="22">
|
||||
<StackPanel Spacing="12">
|
||||
@@ -410,7 +410,7 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Manage Account -->
|
||||
<!-- Manage Account -->
|
||||
<Border Background="{DynamicResource BgSurface}" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="16"
|
||||
Padding="22">
|
||||
<StackPanel Spacing="10">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Margin="32,28,32,0">
|
||||
|
||||
<!-- ── Top Bar ── -->
|
||||
<!-- Top Bar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,24">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Classes="muted" Text="Insights & Trends" />
|
||||
@@ -45,7 +45,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable Content ── -->
|
||||
<!-- Scrollable Content -->
|
||||
<ScrollViewer Grid.Row="1" Name="mainScrollviewer"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
@@ -60,7 +60,7 @@
|
||||
Foreground="{DynamicResource AccentGreen}" FontSize="12" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Section 1: KPI Cards ── -->
|
||||
<!-- Section 1: KPI Cards -->
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Grid > Border">
|
||||
@@ -125,7 +125,7 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Section 2: Cash Flow Trend ── -->
|
||||
<!-- Section 2: Cash Flow Trend -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel>
|
||||
@@ -145,7 +145,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Section 3: Net Worth ── -->
|
||||
<!-- Section 3: Net Worth -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel>
|
||||
@@ -165,7 +165,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Section 4+6: Day of Week + Income Sources ── -->
|
||||
<!-- Section 4+6: Day of Week + Income Sources -->
|
||||
<Grid ColumnDefinitions="*,*" >
|
||||
<!-- Day of Week -->
|
||||
<Border Grid.Column="0" Classes="card" Margin="0,0,10,0">
|
||||
@@ -213,7 +213,7 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Section 5: Top Categories ── -->
|
||||
<!-- Section 5: Top Categories -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel>
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
x:Class="Clario.Views.AuthView">
|
||||
<Grid>
|
||||
|
||||
<!-- Background -->
|
||||
<!-- Background -->
|
||||
<!-- <Calendar SelectionMode="SingleRange"> -->
|
||||
<!-- </Calendar> -->
|
||||
|
||||
|
||||
<!-- <Border Background="{DynamicResource AccentBlue}" VerticalAlignment="Top" HorizontalAlignment="Left" Height="400" Width="400" Padding="10"> -->
|
||||
<!-- -->
|
||||
<!-- -->
|
||||
<!-- </Border> -->
|
||||
|
||||
<!-- Center card -->
|
||||
<!-- Center card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -30,7 +30,7 @@
|
||||
BoxShadow="0 24 64 0 #40000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Logo + App name -->
|
||||
<!-- Logo + App name -->
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
Spacing="0"
|
||||
Margin="0,0,0,32">
|
||||
@@ -43,10 +43,10 @@
|
||||
<!-- REPLACE: app name -->
|
||||
<StackPanel Spacing="4" HorizontalAlignment="Center">
|
||||
<!-- <TextBlock Text="Clario" -->
|
||||
<!-- FontSize="22" -->
|
||||
<!-- FontWeight="Bold" -->
|
||||
<!-- Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Center" /> -->
|
||||
<!-- FontSize="22" -->
|
||||
<!-- FontWeight="Bold" -->
|
||||
<!-- Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Center" /> -->
|
||||
<TextBlock Text="Your personal finance tracker"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
@@ -54,13 +54,14 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Tab switcher -->
|
||||
<!-- Tab switcher -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,26">
|
||||
Margin="0,0,0,26"
|
||||
IsVisible="{Binding ShowTabs}">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<!-- REPLACE: active state driven by IsLoginMode -->
|
||||
<!-- Sign In — active -->
|
||||
@@ -178,13 +179,13 @@
|
||||
</Border>
|
||||
|
||||
<!-- Forgot password -->
|
||||
<!-- REPLACE: Command="{Binding ForgotPasswordCommand}" -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,0,24">
|
||||
Margin="0,0,0,24"
|
||||
Command="{Binding SetOperationCommand}" CommandParameter="forgotPassword">
|
||||
<TextBlock Text="Forgot password?"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
@@ -230,10 +231,129 @@
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ══════════════════════════════════
|
||||
<!-- FORGOT PASSWORD PANEL -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isForgotPassword}">
|
||||
|
||||
<!-- Back button + heading -->
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,0,0,20"
|
||||
Command="{Binding SetOperationCommand}" CommandParameter="login">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/arrow-left.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Back to Sign In"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<TextBlock Text="Reset your password"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Margin="0,0,0,6" />
|
||||
<TextBlock Text="Enter your email and we'll send you a reset link."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Success state -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,10"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding ResetEmailSent}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
|
||||
<TextBlock Text="Check your email for a reset link."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Email -->
|
||||
<TextBlock Text="EMAIL" Classes="label" Margin="0,0,0,6"
|
||||
IsVisible="{Binding !ResetEmailSent}" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,14"
|
||||
IsVisible="{Binding !ResetEmailSent}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/mail.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,10,0" />
|
||||
<TextBox Grid.Column="1" Classes="ghost"
|
||||
Watermark="you@example.com"
|
||||
Text="{Binding Email}"
|
||||
BorderThickness="0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="42"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error message -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,10"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Send Reset Link button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,12"
|
||||
Margin="0,0,0,20"
|
||||
IsVisible="{Binding !ResetEmailSent}"
|
||||
Command="{Binding SendResetLinkCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/mail.svg"
|
||||
Width="15" Height="15"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Send Reset Link"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
SIGN UP PANEL
|
||||
REPLACE: IsVisible="{Binding !IsLoginMode}"
|
||||
══════════════════════════════════ -->
|
||||
-->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
|
||||
|
||||
<!-- Name row -->
|
||||
@@ -430,7 +550,7 @@
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- Footer -->
|
||||
<Separator Margin="0,0,0,16" />
|
||||
<TextBlock Text="Your data is encrypted and synced securely."
|
||||
FontSize="11"
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<vm:BudgetFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<!-- Dim overlay -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<!-- Modal card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -27,7 +27,7 @@
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -62,7 +62,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Category ───────────────────── -->
|
||||
<!-- Category -->
|
||||
<TextBlock Text="CATEGORY" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -96,7 +96,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Limit Amount ────────────────── -->
|
||||
<!-- Limit Amount -->
|
||||
<TextBlock Text="LIMIT AMOUNT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -129,7 +129,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Period ─────────────────────── -->
|
||||
<!-- Period -->
|
||||
<TextBlock Text="PERIOD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -187,7 +187,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Alert Threshold ────────────── -->
|
||||
<!-- Alert Threshold -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,6">
|
||||
<TextBlock Grid.Column="0" Text="ALERT THRESHOLD" Classes="label" />
|
||||
<TextBlock Grid.Column="1"
|
||||
@@ -223,7 +223,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Rollover ───────────────────── -->
|
||||
<!-- Rollover -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -257,7 +257,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -276,7 +276,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete button (edit mode only) ── -->
|
||||
<!-- Delete button (edit mode only) -->
|
||||
<Button Classes="danger"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
@@ -290,7 +290,7 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
@@ -323,7 +323,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete confirm sub-modal ──────────────── -->
|
||||
<!-- Delete confirm sub-modal -->
|
||||
<Grid IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
@@ -338,7 +338,7 @@
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -379,7 +379,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -390,7 +390,7 @@
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
<Design.DataContext>
|
||||
<vm:BudgetViewModel />
|
||||
</Design.DataContext>
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
<!--
|
||||
ROOT
|
||||
═══════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Margin="32,28,32,0">
|
||||
|
||||
<!-- ══════════════════════════════════════════
|
||||
<!--
|
||||
TOP BAR
|
||||
══════════════════════════════════════════ -->
|
||||
-->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="0,0,0,24">
|
||||
@@ -100,16 +100,16 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ══════════════════════════════════════════
|
||||
<!--
|
||||
MAIN CONTENT
|
||||
Left * — budget categories
|
||||
Right 320 — monthly overview panel
|
||||
══════════════════════════════════════════ -->
|
||||
-->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,320">
|
||||
|
||||
<!-- ─────────────────────────────────────
|
||||
<!--
|
||||
LEFT — Budget Categories
|
||||
───────────────────────────────────── -->
|
||||
-->
|
||||
<ScrollViewer Grid.Column="0"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -241,15 +241,15 @@
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─────────────────────────────────────
|
||||
<!--
|
||||
RIGHT — Overview Panel
|
||||
───────────────────────────────────── -->
|
||||
-->
|
||||
<ScrollViewer Grid.Column="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled" Margin="0,0,0,0" Padding="0 0 8 0">
|
||||
<StackPanel Spacing="14" Margin="0 0 0 28">
|
||||
|
||||
<!-- ── Period Overview ───────────────── -->
|
||||
<!-- Period Overview -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -363,7 +363,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Days remaining in period ───────── -->
|
||||
<!-- Days remaining in period -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -421,7 +421,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Spending Breakdown ─────────────── -->
|
||||
<!-- Spending Breakdown -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -459,84 +459,84 @@
|
||||
|
||||
<!-- Row: Food -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource IconBgGreen}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Food & Dining" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$340" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="21%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource IconBgGreen}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Food & Dining" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$340" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="21%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
<!-- Row: Housing -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentOrange}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Housing" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$540" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="34%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentOrange}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Housing" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$540" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="34%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
<!-- Row: Transport -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentBlue}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Transport" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$110" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="7%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentBlue}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Transport" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$110" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="7%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
<!-- Row: Shopping (over) -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentRed}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Shopping" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$380" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="24%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentRed}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Shopping" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$380" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="24%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
<!-- Row: Leisure -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPurple}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Entertainment" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$170" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="11%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPurple}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Entertainment" FontSize="12" Foreground="{DynamicResource TextSecondary}" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$170" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="11%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
<!-- Row: Health -->
|
||||
<!-- <Grid ColumnDefinitions="Auto,*,Auto"> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPink}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Health" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$69" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="4%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- <Border Grid.Column="0" Background="{DynamicResource AccentPink}" CornerRadius="6" Width="10" Height="10" Margin="0,0,10,0" -->
|
||||
<!-- VerticalAlignment="Center" /> -->
|
||||
<!-- <TextBlock Grid.Column="1" Text="Health" FontSize="12" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" /> -->
|
||||
<!-- <StackPanel Grid.Column="2" HorizontalAlignment="Right" Spacing="1"> -->
|
||||
<!-- <TextBlock Text="$69" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" -->
|
||||
<!-- HorizontalAlignment="Right" /> -->
|
||||
<!-- <TextBlock Text="4%" FontSize="10" Foreground="{DynamicResource TextMuted}" HorizontalAlignment="Right" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </Grid> -->
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Savings Goal ───────────────────── -->
|
||||
<!-- Savings Goal -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<vm:CategoryFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<!-- Dim overlay -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<!-- Modal card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -27,7 +27,7 @@
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
@@ -64,7 +64,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Name ──────────────────────── -->
|
||||
<!-- Name -->
|
||||
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Name, Mode=TwoWay}"
|
||||
Watermark="e.g. Groceries"
|
||||
@@ -74,7 +74,7 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- ── Type toggle ─────────────────── -->
|
||||
<!-- Type toggle -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -116,7 +116,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Icon picker ─────────────────── -->
|
||||
<!-- Icon picker -->
|
||||
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -161,7 +161,7 @@
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ── Color ──────────────────────── -->
|
||||
<!-- Color -->
|
||||
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -180,7 +180,7 @@
|
||||
IsAccentColorsVisible="False" />
|
||||
</Border>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -199,7 +199,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete button (edit mode only) ── -->
|
||||
<!-- Delete button (edit mode only) -->
|
||||
<Button Classes="danger"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
@@ -215,7 +215,7 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
@@ -248,7 +248,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete confirm sub-modal ──────────────── -->
|
||||
<!-- Delete confirm sub-modal -->
|
||||
<Grid IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
@@ -262,7 +262,7 @@
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -300,7 +300,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -311,7 +311,7 @@
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
304
Clario/Views/DashboardSkeletonView.axaml
Normal file
304
Clario/Views/DashboardSkeletonView.axaml
Normal file
@@ -0,0 +1,304 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="1180" d:DesignHeight="800"
|
||||
MinWidth="780" MinHeight="600"
|
||||
x:CompileBindings="False"
|
||||
x:Class="Clario.Views.DashboardSkeletonView">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.skeleton">
|
||||
<Setter Property="Background" Value="{DynamicResource BgHover}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.85" IterationCount="INFINITE" PlaybackDirection="Alternate" FillMode="Both">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Opacity" Value="0.35" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid ColumnDefinitions="*">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" Margin="32,28,32,32">
|
||||
|
||||
<!-- Top Bar -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="8">
|
||||
<Border Classes="skeleton" Height="13" Width="155" HorizontalAlignment="Left" />
|
||||
<Border Classes="skeleton" Height="26" Width="210" HorizontalAlignment="Left" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="142" Height="36"
|
||||
CornerRadius="{StaticResource RadiusControl}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- KPI Cards Row -->
|
||||
<Grid ColumnDefinitions="*,*,*" MaxHeight="160">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Grid > Border.card">
|
||||
<Setter Property="Margin" Value="0,0,16,0" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<!-- Monthly Income -->
|
||||
<Border Grid.Column="0" Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="11" Width="105" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton" Height="30" Width="130" CornerRadius="8" />
|
||||
<Border Classes="skeleton" Height="22" Width="80" CornerRadius="20" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Monthly Expenses -->
|
||||
<Border Grid.Column="1" Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="11" Width="125" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton" Height="30" Width="115" CornerRadius="8" />
|
||||
<Border Classes="skeleton" Height="22" Width="80" CornerRadius="20" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Savings Rate -->
|
||||
<Border Grid.Column="2" Classes="card" Margin="0">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="skeleton" Width="28" Height="28" CornerRadius="{StaticResource RadiusIcon}" />
|
||||
<Border Classes="skeleton" Height="11" Width="90" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Border Classes="skeleton" Height="30" Width="80" CornerRadius="8" />
|
||||
<Border Classes="skeleton" Height="7" HorizontalAlignment="Stretch" CornerRadius="4" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Mid Row: Chart + Budget -->
|
||||
<Grid ColumnDefinitions="*,340" MaxHeight="470">
|
||||
|
||||
<!-- Spending Chart -->
|
||||
<Border Grid.Column="0" Classes="card" Margin="0,0,16,0">
|
||||
<StackPanel Spacing="20">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="7">
|
||||
<Border Classes="skeleton" Height="15" Width="175" />
|
||||
<Border Classes="skeleton" Height="12" Width="120" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="105" Height="32"
|
||||
CornerRadius="{StaticResource RadiusIcon}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Chart area -->
|
||||
<Border Classes="skeleton" Height="250" HorizontalAlignment="Stretch" CornerRadius="8" />
|
||||
<!-- Category labels row -->
|
||||
<Grid ColumnDefinitions="*,*,*,*,*" Margin="0,-10,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="2" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="3" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
|
||||
<Border Grid.Column="4" Classes="skeleton" Height="12" Margin="6,0" HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
<!-- Amount labels row -->
|
||||
<Grid ColumnDefinitions="*,*,*,*,*" Margin="0,-8,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="36" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="13" Width="40" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="2" Classes="skeleton" Height="13" Width="32" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="3" Classes="skeleton" Height="13" Width="38" HorizontalAlignment="Center" />
|
||||
<Border Grid.Column="4" Classes="skeleton" Height="13" Width="34" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Budget Tracker -->
|
||||
<Border Grid.Column="1" Classes="card">
|
||||
<StackPanel Spacing="20">
|
||||
<StackPanel Spacing="7">
|
||||
<Border Classes="skeleton" Height="15" Width="115" />
|
||||
<Border Classes="skeleton" Height="12" Width="85" />
|
||||
</StackPanel>
|
||||
<!-- Budget items -->
|
||||
<StackPanel Spacing="22">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="95" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="70" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
<Separator Margin="-8,4" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="72" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="62" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
<Separator Margin="-8,4" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="105" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="67" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
<Separator Margin="-8,4" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="82" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="12" Width="74" />
|
||||
</Grid>
|
||||
<Border Classes="skeleton" Height="6" HorizontalAlignment="Stretch" CornerRadius="3" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Bottom Row: Transactions + Accounts -->
|
||||
<Grid ColumnDefinitions="*,300" MaxHeight="500">
|
||||
|
||||
<!-- Recent Transactions -->
|
||||
<Border Grid.Column="0" Classes="card" Margin="0,0,16,0">
|
||||
<StackPanel Spacing="18">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="7">
|
||||
<Border Classes="skeleton" Height="15" Width="160" />
|
||||
<Border Classes="skeleton" Height="12" Width="115" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="skeleton" Width="58" Height="13" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!-- Transaction rows -->
|
||||
<StackPanel Spacing="18">
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
|
||||
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
|
||||
<Border Classes="skeleton" Height="13" Width="145" />
|
||||
<Border Classes="skeleton" Height="11" Width="105" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="14" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Separator />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
|
||||
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
|
||||
<Border Classes="skeleton" Height="13" Width="180" />
|
||||
<Border Classes="skeleton" Height="11" Width="125" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="55" Height="14" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Separator />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
|
||||
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
|
||||
<Border Classes="skeleton" Height="13" Width="120" />
|
||||
<Border Classes="skeleton" Height="11" Width="88" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="70" Height="14" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Separator />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
|
||||
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
|
||||
<Border Classes="skeleton" Height="13" Width="162" />
|
||||
<Border Classes="skeleton" Height="11" Width="112" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="60" Height="14" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Separator />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4,0,0">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="42" Height="42"
|
||||
CornerRadius="{StaticResource RadiusControl}" Margin="0,0,14,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="7">
|
||||
<Border Classes="skeleton" Height="13" Width="150" />
|
||||
<Border Classes="skeleton" Height="11" Width="98" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="65" Height="14" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Separator />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Accounts Summary -->
|
||||
<Border Grid.Column="1" Classes="card">
|
||||
<Grid RowDefinitions="Auto,*,Auto" RowSpacing="18">
|
||||
<StackPanel Grid.Row="0" Spacing="7">
|
||||
<Border Classes="skeleton" Height="15" Width="78" />
|
||||
<Border Classes="skeleton" Height="12" Width="135" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Spacing="10">
|
||||
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
|
||||
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="92" />
|
||||
<Border Classes="skeleton" Height="11" Width="60" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="58" Height="13" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
|
||||
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="72" />
|
||||
<Border Classes="skeleton" Height="11" Width="52" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="62" Height="13" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BgBase}" CornerRadius="{StaticResource RadiusInset}"
|
||||
Padding="14,12" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Width="36" Height="36"
|
||||
CornerRadius="{StaticResource RadiusIcon}" Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="5">
|
||||
<Border Classes="skeleton" Height="12" Width="82" />
|
||||
<Border Classes="skeleton" Height="11" Width="56" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="2" Classes="skeleton" Width="54" Height="13" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="18" Grid.Row="2">
|
||||
<Separator />
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Border Grid.Column="0" Classes="skeleton" Height="13" Width="82" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="skeleton" Height="18" Width="92" CornerRadius="7" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/Views/DashboardSkeletonView.axaml.cs
Normal file
11
Clario/Views/DashboardSkeletonView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class DashboardSkeletonView : UserControl
|
||||
{
|
||||
public DashboardSkeletonView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
<Grid ColumnDefinitions="*">
|
||||
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Name="mainScrollviewer">
|
||||
<StackPanel Spacing="24" Margin="32,28,32,32">
|
||||
<!-- Top Bar -->
|
||||
<!-- Top Bar -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<!-- <TextBlock Classes="muted" Text="Friday, March 6, 2026" /> -->
|
||||
@@ -30,7 +30,7 @@
|
||||
Cursor="Hand" Content="+ Add Transaction" Command="{Binding CreateTransactionCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- KPI Cards Row -->
|
||||
<!-- KPI Cards Row -->
|
||||
<Grid ColumnDefinitions="*,*,*" HorizontalAlignment="Stretch" MaxHeight="160">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Grid > Border">
|
||||
@@ -109,7 +109,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<!-- Mid Row: Spending Chart + Budget -->
|
||||
<!-- Mid Row: Spending Chart + Budget -->
|
||||
|
||||
<Grid ColumnDefinitions="*,340" MaxHeight="470">
|
||||
<!-- Spending Breakdown -->
|
||||
@@ -258,7 +258,7 @@
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
<!-- Bottom Row: Recent Transactions + Accounts -->
|
||||
<!-- Bottom Row: Recent Transactions + Accounts -->
|
||||
|
||||
<Grid ColumnDefinitions="*,300" MaxHeight="500">
|
||||
<!-- Recent Transactions -->
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<!-- Dim overlay -->
|
||||
<Border Background="#70000000"/>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
<!--
|
||||
STEP 1 — Simple confirm (no transactions)
|
||||
═══════════════════════════════════════ -->
|
||||
-->
|
||||
<Border IsVisible="{Binding IsSimpleConfirmStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -30,7 +30,7 @@
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -99,7 +99,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -110,7 +110,7 @@
|
||||
<TextBlock Text="Delete Account"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -119,9 +119,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
<!--
|
||||
STEP 1B — Has transactions warning
|
||||
═══════════════════════════════════════ -->
|
||||
-->
|
||||
<Border IsVisible="{Binding IsHasTransactionsStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -236,9 +236,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
<!--
|
||||
STEP 2 — Pick target account + confirm
|
||||
═══════════════════════════════════════ -->
|
||||
-->
|
||||
<Border IsVisible="{Binding IsMigrateStep}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -470,7 +470,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
IsEnabled="{Binding CanMigrateAndDelete}"
|
||||
@@ -482,7 +482,7 @@
|
||||
<TextBlock Text="Migrate & Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avaloniaProgressRing="clr-namespace:AvaloniaProgressRing;assembly=AvaloniaProgressRing"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
Background="{DynamicResource BgBase}"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="vm:LoadingViewModel"
|
||||
x:Class="Clario.Views.LoadingView">
|
||||
<Panel>
|
||||
<avaloniaProgressRing:ProgressRing Width="100" Height="100" IsActive="True"
|
||||
Foreground="{DynamicResource AccentBlue}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Panel>
|
||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,*,*">
|
||||
<avaloniaProgressRing:ProgressRing Grid.Row="1" Grid.Column="1"
|
||||
Width="100" Height="100"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsActive="True"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -13,7 +13,7 @@
|
||||
<vm:MainViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid ColumnDefinitions="220,*">
|
||||
<!-- ───────────────────────────────────── SIDEBAR ───────────────────────────────────── -->
|
||||
<!-- SIDEBAR -->
|
||||
<Border Grid.Column="0" Background="{DynamicResource BgSidebar}" BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,1,0" Padding="16,28,16,24" IsEnabled="{Binding !IsTransactionFormVisible}">
|
||||
<DockPanel>
|
||||
@@ -49,9 +49,9 @@
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
</Grid>
|
||||
<!-- <Button Grid.Column="1" Classes="base" Width="24" Height="24" Padding="2" Command="{Binding SignOutCommand}"> -->
|
||||
<!-- <ToolTip.Tip> -->
|
||||
<!-- signout -->
|
||||
<!-- </ToolTip.Tip> -->
|
||||
<!-- <ToolTip.Tip> -->
|
||||
<!-- signout -->
|
||||
<!-- </ToolTip.Tip> -->
|
||||
<!-- </Button> -->
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -122,7 +122,9 @@
|
||||
<views:CategoryFormView
|
||||
DataContext="{Binding CategoryFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
|
||||
|
||||
<views:MessageBoxView
|
||||
DataContext="{Binding MessageBoxViewModel}"
|
||||
IsVisible="{Binding DataContext.IsMessageBoxVisible, ElementName=MainControl}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
92
Clario/Views/MessageBoxView.axaml
Normal file
92
Clario/Views/MessageBoxView.axaml
Normal file
@@ -0,0 +1,92 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
x:Class="Clario.Views.MessageBoxView"
|
||||
x:DataType="vm:MessageBoxViewModel">
|
||||
<Grid>
|
||||
<!-- Dim overlay -->
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- Card -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource RadiusCard}"
|
||||
Padding="28,24"
|
||||
MaxWidth="400"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel Spacing="20">
|
||||
|
||||
<!-- Icon + Title row -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<!-- Error icon -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Width="40" Height="40" Margin="0,0,14,0"
|
||||
IsVisible="{Binding IsError}">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg" Width="18" Height="18" Css="{DynamicResource SvgRed}" />
|
||||
</Border>
|
||||
<!-- Warning icon -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgOrange}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Width="40" Height="40" Margin="0,0,14,0"
|
||||
IsVisible="{Binding IsWarning}">
|
||||
<Svg Path="../Assets/Icons/triangle-alert.svg" Width="18" Height="18" Css="{DynamicResource SvgYellow}" />
|
||||
</Border>
|
||||
<!-- Success icon -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Width="40" Height="40" Margin="0,0,14,0"
|
||||
IsVisible="{Binding IsSuccess}">
|
||||
<Svg Path="../Assets/Icons/circle-check.svg" Width="18" Height="18" Css="{DynamicResource SvgGreen}" />
|
||||
</Border>
|
||||
<!-- Info icon -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{DynamicResource IconBgBlue}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Width="40" Height="40" Margin="0,0,14,0"
|
||||
IsVisible="{Binding IsInfo}">
|
||||
<Svg Path="../Assets/Icons/info.svg" Width="18" Height="18" Css="{DynamicResource SvgBlue}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
VerticalAlignment="Top"
|
||||
Cursor="Hand"
|
||||
Command="{Binding CloseCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Message text -->
|
||||
<TextBlock Text="{Binding Message}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextSecondary}"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="20" />
|
||||
|
||||
<!-- OK button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="20,9"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="OK" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/Views/MessageBoxView.axaml.cs
Normal file
11
Clario/Views/MessageBoxView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class MessageBoxView : UserControl
|
||||
{
|
||||
public MessageBoxView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
233
Clario/Views/ResetPasswordView.axaml
Normal file
233
Clario/Views/ResetPasswordView.axaml
Normal file
@@ -0,0 +1,233 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="1400" d:DesignHeight="1200"
|
||||
x:DataType="vm:ResetPasswordViewModel"
|
||||
x:Class="Clario.Views.ResetPasswordView">
|
||||
<Grid>
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Padding="40"
|
||||
Width="420"
|
||||
BoxShadow="0 24 64 0 #40000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Logo -->
|
||||
<Border CornerRadius="16"
|
||||
Height="80"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,10">
|
||||
<Image Source="{DynamicResource LogoCombinedPrimaryTransparent2x}" />
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Set new password"
|
||||
FontSize="18" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,6" />
|
||||
<TextBlock Text="Enter and confirm your new password below."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,28" />
|
||||
|
||||
<!-- SUCCESS STATE -->
|
||||
<StackPanel IsVisible="{Binding PasswordUpdated}" Spacing="16">
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #2ECC8A; }" />
|
||||
<TextBlock Text="Password updated successfully."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,12"
|
||||
Command="{Binding GoToSignInCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/log-in.svg"
|
||||
Width="15" Height="15"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Sign In"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- FORM STATE -->
|
||||
<StackPanel IsVisible="{Binding !PasswordUpdated}" Spacing="0">
|
||||
|
||||
<!-- New Password -->
|
||||
<TextBlock Text="NEW PASSWORD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/lock.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,10,0" />
|
||||
<TextBox Grid.Column="1" Classes="ghost"
|
||||
Watermark="At least 6 characters"
|
||||
Text="{Binding NewPassword}"
|
||||
PasswordChar="●"
|
||||
RevealPassword="{Binding #showNew.IsChecked}"
|
||||
BorderThickness="0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="42"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2" Name="showNew"
|
||||
Background="Transparent"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
BorderThickness="0" Height="42"
|
||||
Padding="8,0"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center">
|
||||
<ToggleButton.Styles>
|
||||
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
</ToggleButton.Styles>
|
||||
<Panel>
|
||||
<Svg Path="../Assets/Icons/eye.svg"
|
||||
Width="15" Height="15"
|
||||
IsVisible="{Binding #showNew.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="15" Height="15"
|
||||
IsVisible="{Binding !#showNew.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Panel>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<TextBlock Text="CONFIRM PASSWORD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="0"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/lock.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,10,0" />
|
||||
<TextBox Grid.Column="1" Classes="ghost"
|
||||
Watermark="Repeat your password"
|
||||
Text="{Binding ConfirmPassword}"
|
||||
PasswordChar="●"
|
||||
RevealPassword="{Binding #showConfirm.IsChecked}"
|
||||
BorderThickness="0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="42"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2" Name="showConfirm"
|
||||
Background="Transparent"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
BorderThickness="0" Height="42"
|
||||
Padding="8,0"
|
||||
Cursor="Hand"
|
||||
VerticalAlignment="Center">
|
||||
<ToggleButton.Styles>
|
||||
<Style Selector="ToggleButton:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
</ToggleButton.Styles>
|
||||
<Panel>
|
||||
<Svg Path="../Assets/Icons/eye.svg"
|
||||
Width="15" Height="15"
|
||||
IsVisible="{Binding #showConfirm.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="15" Height="15"
|
||||
IsVisible="{Binding !#showConfirm.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Panel>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,10"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Update password button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,12"
|
||||
Margin="0,0,0,20"
|
||||
Command="{Binding SetNewPasswordCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/lock.svg"
|
||||
Width="15" Height="15"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Update Password"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<Separator Margin="0,0,0,16" />
|
||||
<TextBlock Text="Your data is encrypted and synced securely."
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/Views/ResetPasswordView.axaml.cs
Normal file
11
Clario/Views/ResetPasswordView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.Views;
|
||||
|
||||
public partial class ResetPasswordView : UserControl
|
||||
{
|
||||
public ResetPasswordView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
Spacing="0"
|
||||
MaxWidth="720">
|
||||
|
||||
<!-- ── Page header ─────────────────────────── -->
|
||||
<!-- Page header -->
|
||||
<StackPanel Margin="0,0,0,28">
|
||||
<TextBlock Text="Settings"
|
||||
FontSize="26"
|
||||
@@ -29,7 +29,7 @@
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── Global success / error banner ─────────── -->
|
||||
<!-- Global success / error banner -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
@@ -75,9 +75,9 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Profile
|
||||
══════════════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="PROFILE"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
@@ -223,9 +223,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Account Security
|
||||
══════════════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="ACCOUNT & SECURITY"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
@@ -238,7 +238,7 @@
|
||||
Margin="0,0,0,24">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Email row ───────────────────────────── -->
|
||||
<!-- Email row -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="20,0">
|
||||
@@ -350,7 +350,7 @@
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Password row ───────────────────────── -->
|
||||
<!-- Password row -->
|
||||
<Border Padding="20,0">
|
||||
<!-- Normal password display -->
|
||||
<Panel>
|
||||
@@ -473,9 +473,9 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ══════════════════════════════════════════════
|
||||
<!--
|
||||
SECTION: Danger zone
|
||||
══════════════════════════════════════════════ -->
|
||||
-->
|
||||
<TextBlock Text="SESSION"
|
||||
Classes="label"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
<vm:TransactionFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<!-- ── Dim overlay ───────────────────────── -->
|
||||
<!-- Dim overlay -->
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- ── Modal card ────────────────────────── -->
|
||||
<!-- Modal card -->
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -28,7 +28,7 @@
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- ── Header ──────────────────────── -->
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,24">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
@@ -66,7 +66,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Type toggle ─────────────────── -->
|
||||
<!-- Type toggle -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -136,7 +136,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Amount ──────────────────────── -->
|
||||
<!-- Amount -->
|
||||
<TextBlock Text="AMOUNT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -174,7 +174,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Description (hidden for transfers) ─── -->
|
||||
<!-- Description (hidden for transfers) -->
|
||||
<TextBlock Text="DESCRIPTION" Classes="label" Margin="0,0,0,6"
|
||||
IsVisible="{Binding !IsTransfer}" />
|
||||
<TextBox Text="{Binding Description, Mode=TwoWay}"
|
||||
@@ -186,7 +186,7 @@
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}" />
|
||||
|
||||
<!-- ── Category + Account (income/expense) ── -->
|
||||
<!-- Category + Account (income/expense) -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}">
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── From + To accounts (transfer) ── -->
|
||||
<!-- From + To accounts (transfer) -->
|
||||
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding IsTransfer}">
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Exchange Rate (shown for foreign-currency accounts) ── -->
|
||||
<!-- Exchange Rate (shown for foreign-currency accounts) -->
|
||||
<Border IsVisible="{Binding ShowExchangeRateField}"
|
||||
Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -427,7 +427,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Date ────────────────────────── -->
|
||||
<!-- Date -->
|
||||
<TextBlock Text="DATE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -456,7 +456,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Note ────────────────────────── -->
|
||||
<!-- Note -->
|
||||
<TextBlock Text="NOTE (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Note, Mode=TwoWay}"
|
||||
Watermark="Add a note..."
|
||||
@@ -466,7 +466,7 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- ── Budget approaching warning ─────── -->
|
||||
<!-- Budget approaching warning -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
@@ -486,7 +486,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Budget over-limit warning ──────── -->
|
||||
<!-- Budget over-limit warning -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -506,7 +506,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -525,7 +525,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Delete button (edit mode only) ── -->
|
||||
<!-- Delete button (edit mode only) -->
|
||||
<Button Classes="danger"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
@@ -539,7 +539,7 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- ── Actions ──────────────────────── -->
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
@@ -573,7 +573,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- DELETE CONFIRM MODAL -->
|
||||
<!-- ── Delete confirm modal ──────────────── -->
|
||||
<!-- Delete confirm modal -->
|
||||
<Grid IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
@@ -588,7 +588,7 @@
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -629,7 +629,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -640,7 +640,7 @@
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
</Design.DataContext>
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
LEFT PANEL — Summary + Filters
|
||||
═══════════════════════════════════════════════════ -->
|
||||
<!-- LEFT PANEL — Summary + Filters -->
|
||||
<Border Grid.Column="0"
|
||||
Width="260"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
@@ -28,7 +26,7 @@
|
||||
Padding="16,0,16,0">
|
||||
<StackPanel Spacing="0" Margin="0 28 0 28">
|
||||
|
||||
<!-- Period header ─
|
||||
<!-- Period header
|
||||
REPLACE: bind TextBlock texts to SelectedPeriodLabel
|
||||
-->
|
||||
<TextBlock Text="{Binding DateRangeLabel}"
|
||||
@@ -40,7 +38,7 @@
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- Summary stats — left accent bar style ──
|
||||
<!-- Summary stats — left accent bar style
|
||||
REPLACE: bind each amount + count Text
|
||||
-->
|
||||
<StackPanel Spacing="2">
|
||||
@@ -145,7 +143,7 @@
|
||||
<!-- Divider -->
|
||||
<Separator Margin="0,20,0,20" />
|
||||
|
||||
<!-- Filters header + Reset link ──
|
||||
<!-- Filters header + Reset link
|
||||
REPLACE: Command="{Binding ResetFiltersCommand}" on the Button
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||
@@ -165,7 +163,7 @@
|
||||
Command="{Binding ResetFiltersCommand}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Search ──
|
||||
<!-- Search
|
||||
REPLACE: Text="{Binding SearchQuery, Mode=TwoWay}"
|
||||
-->
|
||||
<TextBox Watermark="Search transactions..."
|
||||
@@ -176,7 +174,7 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,14" />
|
||||
|
||||
<!-- Date range ─
|
||||
<!-- Date range
|
||||
REPLACE: SelectedIndex="{Binding SelectedDateRangeIndex}"
|
||||
-->
|
||||
<TextBlock Classes="label" Text="DATE RANGE" Margin="0,0,0,6" />
|
||||
@@ -276,7 +274,7 @@
|
||||
Margin="0,0,0,14">
|
||||
</ComboBox>
|
||||
|
||||
<!-- Account ─
|
||||
<!-- Account
|
||||
REPLACE: ItemsSource="{Binding Accounts}" SelectedItem="{Binding SelectedAccount}"
|
||||
-->
|
||||
<TextBlock Classes="label" Text="ACCOUNT" Margin="0,0,0,6" />
|
||||
@@ -319,9 +317,9 @@
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
<!--
|
||||
RIGHT PANEL — Transaction List
|
||||
═══════════════════════════════════════════════════ -->
|
||||
-->
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*">
|
||||
|
||||
<!-- Top Bar -->
|
||||
@@ -352,7 +350,7 @@
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center"/>
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -488,7 +486,7 @@
|
||||
<Border CornerRadius="6" Padding="6,3">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Opacity="0.15"
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}" />
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}" />
|
||||
</Border.Background>
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
@@ -517,7 +515,7 @@
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<TextBlock Grid.Column="3"
|
||||
<TextBlock Grid.Column="3"
|
||||
Text="{Binding AccountDisplayText}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
|
||||
44
README.md
44
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
# Clario
|
||||
|
||||
**A clean, fast personal finance tracker for the desktop.**
|
||||
**A clean, fast personal finance tracker for desktop and mobile.**
|
||||
|
||||
[](https://github.com/yourusername/clario)
|
||||
[](https://avaloniaui.net/)
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Clario is a native desktop app for tracking personal finances — expenses, income, and budgets — without the clutter of a browser tab. Built with [Avalonia UI](https://avaloniaui.net/), it runs natively on Windows, macOS, and Linux.
|
||||
Clario is a native app for tracking personal finances (expenses, income, and budgets). Built with [Avalonia UI](https://avaloniaui.net/), it runs natively on Windows, Linux, and Android.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,20 +31,25 @@ Clario is a native desktop app for tracking personal finances — expenses, inco
|
||||
|
||||
<!-- Uncomment and replace when ready:
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- 📋 **Expense & income tracking** — Log transactions with categories, amounts, dates, and notes
|
||||
- 📅 **Date range filtering** — Quickly slice your data by day, week, month, or custom range
|
||||
- 🏷️ **Categories** — Organize transactions with custom categories and icons
|
||||
- 📊 **Spending overview** — At-a-glance summaries of where your money is going
|
||||
- 🌙 **Dark-first UI** — Designed for dark mode from the ground up
|
||||
- 🖥️ **Cross-platform** — Runs natively on Windows, macOS, and Linux
|
||||
- **Expense & income tracking** — Log transactions with categories, amounts, dates, and notes
|
||||
- **Date range filtering** — Quickly slice your data by day, week, month, or custom range
|
||||
- **Categories** — Organize transactions with custom categories and icons
|
||||
- **Budget goals** — Set spending limits per category with period tracking
|
||||
- **Analytics** — 6 chart sections covering spending trends, category breakdowns, and more
|
||||
- **Multi-account support** — Track balances across multiple accounts
|
||||
- **Multi-currency** — Accounts in different currencies
|
||||
- **Multiple themes** — Dark, Light, Catppuccin Latte, Macchiato, and Mocha
|
||||
- **Cross-platform** — Runs natively on Windows, Linux, and Android
|
||||
- **Real-time sync** — Powered by Supabase with live data updates
|
||||
|
||||
---
|
||||
|
||||
@@ -54,7 +59,7 @@ Clario is a native desktop app for tracking personal finances — expenses, inco
|
||||
|---|---|
|
||||
| UI Framework | [Avalonia UI 11](https://avaloniaui.net/) |
|
||||
| Language | C# / .NET 8 |
|
||||
| Architecture | MVVM |
|
||||
| Architecture | MVVM (CommunityToolkit.MVVM) |
|
||||
| Icons | [Lucide](https://lucide.dev/) |
|
||||
|
||||
---
|
||||
@@ -68,7 +73,7 @@ If you'd prefer to run from source:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/clario.git
|
||||
cd clario
|
||||
dotnet run --project Clario
|
||||
dotnet run --project Clario.Desktop
|
||||
```
|
||||
|
||||
> Requires the [.NET 8 SDK](https://dotnet.microsoft.com/download) when running from source.
|
||||
@@ -77,15 +82,20 @@ dotnet run --project Clario
|
||||
|
||||
## Project Status
|
||||
|
||||
Clario is in active development. Core tracking features work, but some things are still being built out:
|
||||
Clario is in active development. Core features are working:
|
||||
|
||||
- [x] Transaction entry & editing
|
||||
- [x] Category management
|
||||
- [x] Date range picker
|
||||
- [x] Expense list with filtering
|
||||
- [x] Budget goals
|
||||
- [x] Transaction list with filtering, search & sorting
|
||||
- [x] Budget goals with period navigation
|
||||
- [x] Analytics (charts & spending reports)
|
||||
- [x] Multi-account management
|
||||
- [x] Multi-currency support
|
||||
- [x] Settings (profile, theme, currency, savings goal)
|
||||
- [x] Android support
|
||||
- [x] Multiple themes (Dark, Light, Catppuccin variants)
|
||||
- [ ] Budget goal notifications
|
||||
- [ ] Charts & reports
|
||||
- [ ] CSV import / export
|
||||
- [ ] Recurring transactions
|
||||
|
||||
@@ -99,6 +109,6 @@ The project isn't formally open to contributions yet while the core is still bei
|
||||
|
||||
<div align="center">
|
||||
|
||||
Made with ☕ and Avalonia UI.
|
||||
Made with Avalonia UI.
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user