Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6efa72745 | |||
| 2ce47ee305 | |||
| 90b2abd587 | |||
| 61ff949c19 |
@@ -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
@@ -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
@@ -7,4 +7,7 @@ obj/
|
||||
./Clario/CLAUDE_CONTEXT.md
|
||||
publish/
|
||||
*.tar.gz
|
||||
Clario/devsettings.json
|
||||
Clario/devsettings.json
|
||||
.env
|
||||
TODO.md
|
||||
clario.keystore
|
||||
@@ -20,6 +20,7 @@
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="QuestPDF" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing"/>
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
|
||||
<PackageReference Include="QuestPDF" />
|
||||
<PackageReference Include="SkiaSharp"/>
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="QuestPDF" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
|
||||
@@ -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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="QuestPDF" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
Clario/Assets/Icons/bike.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="18.5" cy="17.5" r="3.5" />
|
||||
<circle cx="5.5" cy="17.5" r="3.5" />
|
||||
<circle cx="15" cy="5" r="1" />
|
||||
<path d="M12 17.5V14l-3-3 4-3 2 3h2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |
14
Clario/Assets/Icons/book-open.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 7v14" />
|
||||
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
19
Clario/Assets/Icons/bus.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 6v6" />
|
||||
<path d="M15 6v6" />
|
||||
<path d="M2 12h19.6" />
|
||||
<path d="M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3" />
|
||||
<circle cx="7" cy="18" r="2" />
|
||||
<path d="M9 18h5" />
|
||||
<circle cx="16" cy="18" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 492 B |
14
Clario/Assets/Icons/camera.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
|
||||
<circle cx="12" cy="13" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
16
Clario/Assets/Icons/coffee.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 2v2" />
|
||||
<path d="M14 2v2" />
|
||||
<path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1" />
|
||||
<path d="M6 2v2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 379 B |
17
Clario/Assets/Icons/dumbbell.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z" />
|
||||
<path d="m2.5 21.5 1.4-1.4" />
|
||||
<path d="m20.1 3.9 1.4-1.4" />
|
||||
<path d="M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z" />
|
||||
<path d="m9.6 14.4 4.8-4.8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
20
Clario/Assets/Icons/film.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M7 3v18" />
|
||||
<path d="M3 7.5h4" />
|
||||
<path d="M3 12h18" />
|
||||
<path d="M3 16.5h4" />
|
||||
<path d="M17 3v18" />
|
||||
<path d="M17 7.5h4" />
|
||||
<path d="M17 16.5h4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
16
Clario/Assets/Icons/gift.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 7v14" />
|
||||
<path d="M20 11v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8" />
|
||||
<path d="M7.5 7a1 1 0 0 1 0-5A4.8 8 0 0 1 12 7a4.8 8 0 0 1 4.5-5 1 1 0 0 1 0 5" />
|
||||
<rect x="3" y="7" width="18" height="4" rx="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
15
Clario/Assets/Icons/graduation-cap.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z" />
|
||||
<path d="M22 10v6" />
|
||||
<path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 412 B |
13
Clario/Assets/Icons/headphones.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 347 B |
14
Clario/Assets/Icons/leaf.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z" />
|
||||
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
15
Clario/Assets/Icons/music.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9 18V5l12-2v13" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
16
Clario/Assets/Icons/package.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
||||
<path d="M12 22V12" />
|
||||
<polyline points="3.29 7 12 12 20.71 7" />
|
||||
<path d="m7.5 4.27 9 5.15" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
17
Clario/Assets/Icons/pizza.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m12 14-1 1" />
|
||||
<path d="m13.75 18.25-1.25 1.42" />
|
||||
<path d="M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12" />
|
||||
<path d="M18.8 9.3a1 1 0 0 0 2.1 7.7" />
|
||||
<path d="M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 506 B |
13
Clario/Assets/Icons/plane.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
17
Clario/Assets/Icons/scissors.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="6" cy="6" r="3" />
|
||||
<path d="M8.12 8.12 12 12" />
|
||||
<path d="M20 4 8.12 15.88" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<path d="M14.8 14.8 20 20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
13
Clario/Assets/Icons/shirt.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
14
Clario/Assets/Icons/smartphone.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
|
||||
<path d="M12 18h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
17
Clario/Assets/Icons/stethoscope.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 2v2" />
|
||||
<path d="M5 2v2" />
|
||||
<path d="M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1" />
|
||||
<path d="M8 15a6 6 0 0 0 12 0v-3" />
|
||||
<circle cx="20" cy="10" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
18
Clario/Assets/Icons/train-front.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 3.1V7a4 4 0 0 0 8 0V3.1" />
|
||||
<path d="m9 15-1-1" />
|
||||
<path d="m15 15 1-1" />
|
||||
<path d="M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z" />
|
||||
<path d="m8 19-2 3" />
|
||||
<path d="m16 19 2 3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
14
Clario/Assets/Icons/tv.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m17 2-5 5-5-5" />
|
||||
<rect width="20" height="15" x="2" y="7" rx="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
16
Clario/Assets/Icons/wine.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 22h8" />
|
||||
<path d="M7 10h10" />
|
||||
<path d="M12 15v7" />
|
||||
<path d="M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
13
Clario/Assets/Icons/wrench.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 417 B |
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net8.0;net8.0-android</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia"/>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" />
|
||||
<PackageReference Include="Avalonia.Svg.Skia" />
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker"/>
|
||||
<PackageReference Include="Avalonia.Svg.Skia"/>
|
||||
<PackageReference Include="Avalonia.Themes.Fluent"/>
|
||||
<PackageReference Include="Avalonia.Fonts.Inter"/>
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
@@ -23,31 +23,48 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm"/>
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
|
||||
<PackageReference Include="Supabase" />
|
||||
<PackageReference Include="Xaml.Behaviors.Interactions" />
|
||||
<PackageReference Include="Xaml.Behaviors.Interactivity" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" />
|
||||
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
|
||||
<PackageReference Include="FluentAvalonia.ProgressRing"/>
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
|
||||
<PackageReference Include="QuestPDF" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
|
||||
<PackageReference Include="Supabase"/>
|
||||
<PackageReference Include="Xaml.Behaviors.Interactions"/>
|
||||
<PackageReference Include="Xaml.Behaviors.Interactivity"/>
|
||||
<PackageReference Include="SkiaSharp"/>
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux"/>
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
|
||||
<DependentUpon>MobileMainView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\AccountFormView.axaml.cs">
|
||||
<DependentUpon>AccountFormView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
|
||||
<DependentUpon>MobileMainView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Views\AccountFormView.axaml.cs">
|
||||
<DependentUpon>AccountFormView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\CategoryFormView.axaml.cs">
|
||||
<DependentUpon>CategoryFormView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\AnalyticsView.axaml.cs">
|
||||
<DependentUpon>AnalyticsView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\DashboardSkeletonView.axaml.cs">
|
||||
<DependentUpon>DashboardSkeletonView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<None Update="devsettings.json">
|
||||
<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;
|
||||
@@ -129,20 +132,187 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InsertTransfer(Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!);
|
||||
var pairId = Guid.NewGuid();
|
||||
|
||||
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
|
||||
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
|
||||
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
|
||||
|
||||
var outTx = new Transaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
AccountId = fromAccountId,
|
||||
Type = "transfer_out",
|
||||
Amount = amount,
|
||||
Description = "Transfer",
|
||||
Note = note?.Trim(),
|
||||
Date = date,
|
||||
TransferPairId = pairId,
|
||||
};
|
||||
var inTx = new Transaction
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
AccountId = toAccountId,
|
||||
Type = "transfer_in",
|
||||
Amount = toAmount,
|
||||
Description = "Transfer",
|
||||
Note = note?.Trim(),
|
||||
Date = date,
|
||||
TransferPairId = pairId,
|
||||
};
|
||||
|
||||
var outResult = await SupabaseService.Client.From<Transaction>().Insert(outTx);
|
||||
var inResult = await SupabaseService.Client.From<Transaction>().Insert(inTx);
|
||||
|
||||
if (outResult.Models.Count >= 1)
|
||||
{
|
||||
var enriched = LinkTransactionCategories(outResult.Models[0]);
|
||||
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();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateTransfer(Guid transferPairId, Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
|
||||
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
|
||||
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
|
||||
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
|
||||
|
||||
foreach (var tx in pair)
|
||||
{
|
||||
tx.AccountId = tx.Type == "transfer_out" ? fromAccountId : toAccountId;
|
||||
tx.Amount = tx.Type == "transfer_in" ? toAmount : amount;
|
||||
tx.Date = date;
|
||||
tx.Note = note?.Trim();
|
||||
var result = await SupabaseService.Client.From<Transaction>().Update(tx);
|
||||
if (result.Model is null) continue;
|
||||
var index = Transactions.IndexOf(tx);
|
||||
if (index != -1)
|
||||
{
|
||||
var enriched = LinkTransactionCategories(result.Model);
|
||||
LinkTransactionAccounts(enriched);
|
||||
Transactions[index] = enriched;
|
||||
}
|
||||
}
|
||||
|
||||
LinkTransactionAccounts();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteTransfer(Guid transferPairId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Transaction>()
|
||||
.Where(x => x.TransferPairId == transferPairId)
|
||||
.Delete();
|
||||
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
|
||||
foreach (var tx in pair)
|
||||
Transactions.Remove(tx);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Category>> FetchCategories(bool forceRefresh = false)
|
||||
{
|
||||
if (Categories.Count != 0 && !forceRefresh) return Categories.ToList();
|
||||
|
||||
var categories = await SupabaseService.Client.From<Category>().Get();
|
||||
Categories = new ObservableCollection<Category>(categories.Models);
|
||||
return categories.Models;
|
||||
}
|
||||
|
||||
public async Task<Category?> InsertCategory(Category category)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Category>()
|
||||
.Insert(category, new QueryOptions() { Returning = QueryOptions.ReturnType.Representation });
|
||||
if (result.Model is null) return null;
|
||||
Categories.Add(result.Model);
|
||||
return result.Model;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateCategory(Category category)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await SupabaseService.Client.From<Category>().Update(category);
|
||||
if (result.Model is null) return;
|
||||
var item = Categories.FirstOrDefault(x => x.Id == result.Model.Id);
|
||||
if (item is null) return;
|
||||
var index = Categories.IndexOf(item);
|
||||
if (index != -1) Categories[index] = result.Model;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteCategory(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.From<Category>().Where(x => x.Id == id).Delete();
|
||||
var item = Categories.FirstOrDefault(x => x.Id == id);
|
||||
if (item is null) return;
|
||||
Categories.Remove(item);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Account>> FetchAccounts(bool forceRefresh = false)
|
||||
{
|
||||
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)
|
||||
@@ -294,7 +464,7 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
|
||||
var balance = accountResult.OpeningBalance +
|
||||
transactionsResult.Sum(t =>
|
||||
t.Type == "income" ? t.Amount : -t.Amount);
|
||||
t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
|
||||
|
||||
accountResult.CurrentBalance = balance;
|
||||
await SupabaseService.Client
|
||||
@@ -399,6 +569,20 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
WeakReferenceMessenger.Default.Send(new RatesRefreshed());
|
||||
}
|
||||
|
||||
/// Converts <paramref name="amount"/> from <paramref name="fromCurrency"/> to
|
||||
/// <paramref name="toCurrency"/> using the current live rates.
|
||||
/// Falls back to <paramref name="amount"/> unchanged when currencies match or rates are missing.
|
||||
private static decimal ConvertAmount(decimal amount, string fromCurrency, string toCurrency)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fromCurrency) || string.IsNullOrEmpty(toCurrency)) return amount;
|
||||
if (fromCurrency.Equals(toCurrency, StringComparison.OrdinalIgnoreCase)) return amount;
|
||||
if (!CurrencyService.LiveRates.TryGetValue(fromCurrency, out var fromRate)) return amount;
|
||||
if (!CurrencyService.LiveRates.TryGetValue(toCurrency, out var toRate) || toRate == 0) return amount;
|
||||
// fromRate = 1 fromCurrency in primary; toRate = 1 toCurrency in primary
|
||||
// amount * fromRate / toRate = amount converted to toCurrency
|
||||
return Math.Round(amount * fromRate / toRate, 6);
|
||||
}
|
||||
|
||||
public void LinkTransactionCategories()
|
||||
{
|
||||
foreach (var transaction in Transactions)
|
||||
@@ -441,6 +625,19 @@ public partial class GeneralDataRepo : ObservableObject
|
||||
tx.OriginalAmountFormatted = tx.IsMultiCurrency
|
||||
? $"{CurrencyService.GetSymbol(accountCurrency)}{tx.Amount:N2}"
|
||||
: string.Empty;
|
||||
|
||||
if (tx.IsTransfer && tx.TransferPairId.HasValue)
|
||||
{
|
||||
var counterpart = Transactions.FirstOrDefault(t => t.TransferPairId == tx.TransferPairId && t.Id != tx.Id);
|
||||
var counterpartAccount = counterpart is not null ? Accounts.FirstOrDefault(a => a.Id == counterpart.AccountId) : null;
|
||||
var fromName = tx.IsTransferOut ? (account?.Name ?? "?") : (counterpartAccount?.Name ?? "?");
|
||||
var toName = tx.IsTransferOut ? (counterpartAccount?.Name ?? "?") : (account?.Name ?? "?");
|
||||
tx.AccountDisplayText = $"{fromName} → {toName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
tx.AccountDisplayText = account?.Name ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateSavingsGoal(decimal? goal)
|
||||
@@ -528,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");
|
||||
}
|
||||
}
|
||||
14
Clario/Enums/AuthError.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Clario.Enums;
|
||||
|
||||
public enum AuthError
|
||||
{
|
||||
InvalidCredentials,
|
||||
EmailAlreadyExists,
|
||||
EmailNotConfirmed,
|
||||
WeakPassword,
|
||||
InvalidEmail,
|
||||
SignupDisabled,
|
||||
RateLimited,
|
||||
SessionExpired,
|
||||
Unknown
|
||||
}
|
||||
408
Clario/MobileViews/AccountFormViewMobile.axaml
Normal file
@@ -0,0 +1,408 @@
|
||||
<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:cc="clr-namespace:Clario.CustomControls"
|
||||
xmlns:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.AccountFormViewMobile"
|
||||
x:DataType="vm:AccountFormViewModel"
|
||||
x:Name="AccountFormRoot"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:AccountFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
|
||||
<!-- Close -->
|
||||
<Button Grid.Column="0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Width="36" Height="36"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
|
||||
<!-- Title -->
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock Text="{Binding FormTitle}"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="{Binding FormSubtitle}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Placeholder to keep title centered -->
|
||||
<Border Grid.Column="2" Width="36" />
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Margin="0,16,0,0">
|
||||
<StackPanel Margin="16,0,16,16" Spacing="0">
|
||||
|
||||
<!-- Icon preview + Name -->
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="0,0,0,20">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedIcon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="22" Height="22"
|
||||
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding Name, Mode=TwoWay}"
|
||||
Watermark="Account name"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0,0,0,1"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
Padding="0,0,0,8"
|
||||
VerticalContentAlignment="Bottom"
|
||||
CornerRadius="0" />
|
||||
</Grid>
|
||||
|
||||
<!-- Type -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,20">
|
||||
<ComboBox ItemsSource="{Binding AccountTypes}"
|
||||
SelectedItem="{Binding SelectedType, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="14,12"
|
||||
FontSize="15"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
|
||||
<!-- Institution -->
|
||||
<TextBlock Text="INSTITUTION (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Institution, Mode=TwoWay}"
|
||||
Watermark="e.g. Chase"
|
||||
FontSize="15"
|
||||
Height="48"
|
||||
Padding="14,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Last 4 digits -->
|
||||
<TextBlock Text="LAST 4 DIGITS (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Mask, Mode=TwoWay}"
|
||||
Watermark="e.g. 1234"
|
||||
FontSize="15"
|
||||
Height="48"
|
||||
Padding="14,0"
|
||||
MaxLength="4"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,20">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
|
||||
<!-- Opening Balance -->
|
||||
<TextBlock Text="OPENING BALANCE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,0"
|
||||
Margin="0,0,0,20"
|
||||
Height="52">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding OpeningBalance, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Currency -->
|
||||
<TextBlock Text="CURRENCY" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding CurrencySearch, Mode=TwoWay}"
|
||||
Watermark="Search currency..."
|
||||
FontSize="14"
|
||||
Height="42"
|
||||
Padding="14,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="8"
|
||||
Margin="0,0,0,20">
|
||||
<ScrollViewer MaxHeight="100"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding FilteredCurrencies}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Classes="nav"
|
||||
Content="{Binding}"
|
||||
Margin="2"
|
||||
Padding="8,6"
|
||||
CornerRadius="6"
|
||||
FontSize="13"
|
||||
Command="{Binding DataContext.SetCurrencyCommand, ElementName=AccountFormRoot}"
|
||||
CommandParameter="{Binding}">
|
||||
<Classes.accented>
|
||||
<MultiBinding Converter="{StaticResource EqualValueConverter}">
|
||||
<Binding Path="." />
|
||||
<Binding Path="DataContext.Currency" ElementName="AccountFormRoot" />
|
||||
</MultiBinding>
|
||||
</Classes.accented>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Credit Limit -->
|
||||
<StackPanel Spacing="6" Margin="0,0,0,20" IsVisible="{Binding IsCredit}">
|
||||
<TextBlock Text="CREDIT LIMIT (OPTIONAL)" Classes="label" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,0"
|
||||
Height="52">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,6,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding CreditLimit, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Opened At -->
|
||||
<TextBlock Text="OPENED ON (OPTIONAL)" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,20">
|
||||
<cc:DateRangePicker Classes="ghost"
|
||||
SelectionMode="SingleDate"
|
||||
SelectedDates="{Binding OpenedAtDates}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="14,12" />
|
||||
</Border>
|
||||
|
||||
<!-- Icon -->
|
||||
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="32" Height="32"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedIcon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding Icons}"
|
||||
SelectedItem="{Binding SelectedIcon, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="10,12"
|
||||
FontSize="15"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Color -->
|
||||
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Height="48"
|
||||
Margin="0,0,0,20">
|
||||
<ColorPicker
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Height="48"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
IsAlphaEnabled="False"
|
||||
IsAlphaVisible="False"
|
||||
IsColorPaletteVisible="False"
|
||||
IsAccentColorsVisible="False" />
|
||||
</Border>
|
||||
|
||||
<!-- Primary account toggle -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,12"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="Primary Account"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Sets this account's currency as the reference currency"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding IsPrimary, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent=""
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,8"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12,16,20">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0"
|
||||
Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="{Binding SaveButtonLabel}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/AccountFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AccountFormViewMobile : UserControl
|
||||
{
|
||||
public AccountFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
xmlns:mobileViews="clr-namespace:Clario.MobileViews"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Clario.MobileViews.AccountsViewMobile"
|
||||
x:DataType="vm:AccountsViewModel"
|
||||
@@ -16,7 +17,7 @@
|
||||
<!-- Root grid — content + overlay stacked -->
|
||||
<Grid>
|
||||
|
||||
<!-- ── Main content ───────────────────────── -->
|
||||
<!-- Main content -->
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
@@ -30,14 +31,24 @@
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
Padding="12,8"
|
||||
VerticalAlignment="Center">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
</Button>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Classes="base"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasArchivedAccounts}"
|
||||
Command="{Binding ShowArchivedListCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Archived" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondary}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Classes="accented"
|
||||
Padding="12,8"
|
||||
Command="{Binding CreateAccountCommand}">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Account list -->
|
||||
@@ -70,11 +81,17 @@
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding TotalBalance, StringFormat='$0.00'}"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentBlue}"
|
||||
VerticalAlignment="Center" />
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N2}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="TotalBalance" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -119,10 +136,23 @@
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="3">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Border IsVisible="{Binding IsPrimary}"
|
||||
CornerRadius="4"
|
||||
Padding="5,2"
|
||||
Background="{DynamicResource IconBgGreen}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="PRIMARY"
|
||||
FontSize="9"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentGreen}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Institution}"
|
||||
FontSize="12"
|
||||
@@ -137,7 +167,7 @@
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="3">
|
||||
<TextBlock Text="{Binding CurrentBalance, StringFormat='$0.00'}"
|
||||
<TextBlock Text="{Binding CurrentBalanceFormatted}"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
@@ -159,7 +189,7 @@
|
||||
</MultiBinding>
|
||||
</Svg.Css>
|
||||
</Svg>
|
||||
<TextBlock Text="{Binding MonthlyIncrease, Converter={StaticResource DecimalSignConverter}}"
|
||||
<TextBlock Text="{Binding MonthlyIncreaseFormatted}"
|
||||
FontSize="11">
|
||||
<TextBlock.Foreground>
|
||||
<MultiBinding Converter="{StaticResource DecimalColorConverter}">
|
||||
@@ -182,7 +212,7 @@
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Bottom sheet overlay ───────────────── -->
|
||||
<!-- Bottom sheet overlay -->
|
||||
<Grid IsVisible="False"
|
||||
x:Name="OverlayGrid">
|
||||
|
||||
@@ -239,20 +269,32 @@
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<!-- Close button -->
|
||||
<Button Grid.Column="2"
|
||||
Background="{DynamicResource BgBase}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="20"
|
||||
Width="34" Height="34"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
x:Name="CloseButton">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
<!-- Edit + Close buttons -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Top">
|
||||
<Button Background="{DynamicResource BgBase}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="20"
|
||||
Width="34" Height="34"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding EditAccountCommand}"
|
||||
CommandParameter="{Binding SelectedAccount}">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
<Button Background="{DynamicResource BgBase}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="20"
|
||||
Width="34" Height="34"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
x:Name="CloseButton">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -269,7 +311,7 @@
|
||||
Padding="16,14">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="CURRENT BALANCE" Classes="label" />
|
||||
<TextBlock Text="{Binding SelectedAccount.CurrentBalance, StringFormat='$0.00'}"
|
||||
<TextBlock Text="{Binding SelectedAccount.CurrentBalanceFormatted}"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
@@ -329,7 +371,7 @@
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
|
||||
<TextBlock Text="Money In" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding SelectedAccount.TotalIncomeThisMonth, StringFormat='$0.00'}" FontSize="14"
|
||||
<TextBlock Text="{Binding SelectedAccount.TotalIncomeFormatted}" FontSize="14"
|
||||
FontWeight="SemiBold" Foreground="{DynamicResource AccentGreen}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.IncomeTransactionsThisMonth}" FontSize="11"
|
||||
@@ -342,7 +384,7 @@
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="1">
|
||||
<TextBlock Text="Money Out" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding SelectedAccount.TotalExpenseThisMonth, StringFormat='$0.00'}" FontSize="14"
|
||||
<TextBlock Text="{Binding SelectedAccount.TotalExpenseFormatted}" FontSize="14"
|
||||
FontWeight="SemiBold" Foreground="{DynamicResource AccentRed}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding SelectedAccount.ExpenseTransactionsThisMonth}" FontSize="11"
|
||||
@@ -446,7 +488,9 @@
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Manage" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimary}" Margin="0,0,0,2" />
|
||||
<Button Background="Transparent" BorderBrush="{DynamicResource BorderSubtle}" BorderThickness="1" CornerRadius="10"
|
||||
Padding="14,10" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left">
|
||||
Padding="14,10" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Command="{Binding RequestArchiveAccountCommand}"
|
||||
CommandParameter="{Binding SelectedAccount}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/archive.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
|
||||
<StackPanel Spacing="1">
|
||||
@@ -456,13 +500,18 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Background="#2A0D0D" BorderBrush="#3A1515" BorderThickness="1" CornerRadius="10" Padding="14,10"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left">
|
||||
<Button Classes="danger"
|
||||
CornerRadius="10"
|
||||
Padding="14,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Command="{Binding RequestDeleteAccountCommand}"
|
||||
CommandParameter="{Binding SelectedAccount}"
|
||||
IsEnabled="{Binding CanDeleteAccount}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="14" Height="14" Css="{DynamicResource SvgRed}" />
|
||||
<StackPanel Spacing="1">
|
||||
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" Foreground="#FF5E5E" />
|
||||
<TextBlock Text="Delete Account" FontSize="12" FontWeight="SemiBold" />
|
||||
<TextBlock Text="Permanently removes all data" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
@@ -476,5 +525,12 @@
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Dialog overlays -->
|
||||
<mobileViews:DeleteAccountDialogViewMobile IsVisible="{Binding DataContext.IsDeleteDialogVisible, ElementName=AccountsPage}"
|
||||
DataContext="{Binding DeleteDialog}" />
|
||||
<mobileViews:ArchiveAccountDialogViewMobile IsVisible="{Binding IsArchiveDialogVisible}" />
|
||||
<mobileViews:ArchivedAccountsDialogViewMobile IsVisible="{Binding IsArchivedListVisible}" />
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
@@ -7,12 +7,13 @@ using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Clario.Models;
|
||||
using Clario.ViewModels;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AccountsViewMobile : UserControl
|
||||
{
|
||||
private bool _sheetVisible = false;
|
||||
private bool _sheetVisible;
|
||||
|
||||
private TranslateTransform SheetTranslate =>
|
||||
(TranslateTransform)BottomSheet.RenderTransform!;
|
||||
@@ -24,10 +25,32 @@ public partial class AccountsViewMobile : UserControl
|
||||
DimOverlay.PointerPressed += async (_, _) => await HideSheet();
|
||||
CloseButton.Click += async (_, _) => await HideSheet();
|
||||
|
||||
AddHandler(Button.ClickEvent, async (sender, e) =>
|
||||
AddHandler(Button.ClickEvent, async (_, e) =>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
Clario/MobileViews/AnalyticsViewMobile.axaml
Normal file
@@ -0,0 +1,161 @@
|
||||
<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"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
Classes="mobile"
|
||||
x:DataType="vm:AnalyticsViewModel"
|
||||
x:Class="Clario.MobileViews.AnalyticsViewMobile">
|
||||
<Design.DataContext>
|
||||
<vm:AnalyticsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,12,16,12">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Classes="muted" Text="Insights & Trends" FontSize="12" />
|
||||
<TextBlock Text="Analytics" FontSize="22" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding PeriodOptions}"
|
||||
SelectedItem="{Binding SelectedPeriod}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Foreground="{DynamicResource TextSecondary}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="8,5" FontSize="12"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="14" Margin="16,0,16,24">
|
||||
|
||||
<!-- KPI Cards (2x2 grid) -->
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
|
||||
<!-- Total Income -->
|
||||
<Border Grid.Row="0" Grid.Column="0" Classes="card" Margin="0,0,7,7">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="INCOME" />
|
||||
<TextBlock Text="{Binding TotalIncomeFormatted}" FontSize="16" FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentGreen}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Total Expenses -->
|
||||
<Border Grid.Row="0" Grid.Column="1" Classes="card" Margin="7,0,0,7">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="EXPENSES" />
|
||||
<TextBlock Text="{Binding TotalExpensesFormatted}" FontSize="16" FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentRed}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Net Savings -->
|
||||
<Border Grid.Row="1" Grid.Column="0" Classes="card" Margin="0,0,7,0">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="NET SAVINGS" />
|
||||
<TextBlock Text="{Binding NetSavingsFormatted}" FontSize="16" FontWeight="Bold"
|
||||
Foreground="{Binding NetSavingsPositive, Converter={StaticResource BoolToColorConverter}, ConverterParameter='#2ECC8A|#FF5E5E'}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- Savings Rate -->
|
||||
<Border Grid.Row="1" Grid.Column="1" Classes="card" Margin="7,0,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="SAVINGS RATE" />
|
||||
<TextBlock Text="{Binding SavingsRateFormatted}" FontSize="16" FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentPurple}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Cash Flow Chart -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Cash Flow Trend" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<lvc:CartesianChart Series="{Binding CashFlowSeries}"
|
||||
XAxes="{Binding CashFlowXAxes}"
|
||||
YAxes="{Binding CashFlowYAxes}"
|
||||
Height="200"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
LegendPosition="Bottom"
|
||||
TooltipPosition="Top"
|
||||
ZoomMode="None"
|
||||
AnimationsSpeed="00:00:00.2" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Net Worth -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Net Worth" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<lvc:CartesianChart Series="{Binding NetWorthSeries}"
|
||||
XAxes="{Binding NetWorthXAxes}"
|
||||
YAxes="{Binding NetWorthYAxes}"
|
||||
Height="180"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
LegendPosition="Hidden"
|
||||
TooltipPosition="Top"
|
||||
ZoomMode="None"
|
||||
AnimationsSpeed="00:00:00.2" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Top Categories -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Top Spending Categories" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
|
||||
<TextBlock Text="No expense data for this period."
|
||||
Classes="muted" IsVisible="{Binding !HasTopCategories}" FontSize="12" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding TopCategories}"
|
||||
IsVisible="{Binding HasTopCategories}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:CategorySpendRow">
|
||||
<Border Background="{DynamicResource BgHover}" CornerRadius="10"
|
||||
Padding="12,10" Margin="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" CornerRadius="7" Width="30" Height="30" Margin="0,0,10,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="14" Height="14"
|
||||
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="4">
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<ProgressBar Value="{Binding Percentage}" Minimum="0" Maximum="100"
|
||||
Height="3" Classes="blue" CornerRadius="2" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding AmountFormatted}"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/AnalyticsViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AnalyticsViewMobile : UserControl
|
||||
{
|
||||
public AnalyticsViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
123
Clario/MobileViews/ArchiveAccountDialogViewMobile.axaml
Normal file
@@ -0,0 +1,123 @@
|
||||
<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"
|
||||
x:Class="Clario.MobileViews.ArchiveAccountDialogViewMobile"
|
||||
x:DataType="vm:AccountsViewModel"
|
||||
x:CompileBindings="False"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:AccountsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<Border VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentOrange}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,40">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Handle bar -->
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="{DynamicResource IconBgOrange}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/archive.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgOrange}" />
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Archive Account"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- Account badge -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,14">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border CornerRadius="7" Width="26" Height="26" VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding AccountToArchive.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding AccountToArchive.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding AccountToArchive.Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Description -->
|
||||
<TextBlock Text="This account will be hidden from your active list and won't appear when adding transactions. You can restore it anytime."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelArchiveCommand}" />
|
||||
<Button Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="{DynamicResource AccentOrange}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmArchiveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/archive.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Archive"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/ArchiveAccountDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ArchiveAccountDialogViewMobile : UserControl
|
||||
{
|
||||
public ArchiveAccountDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
138
Clario/MobileViews/ArchivedAccountsDialogViewMobile.axaml
Normal file
@@ -0,0 +1,138 @@
|
||||
<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"
|
||||
x:Class="Clario.MobileViews.ArchivedAccountsDialogViewMobile"
|
||||
x:Name="ArchivedListView"
|
||||
x:DataType="vm:AccountsViewModel"
|
||||
x:CompileBindings="False"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:AccountsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<Border VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,32">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Handle bar -->
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,18" />
|
||||
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="Archived Accounts"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Hidden from active lists and transaction forms"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
VerticalAlignment="Top"
|
||||
Command="{Binding CloseArchivedListCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Accounts list -->
|
||||
<ScrollViewer MaxHeight="400" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding ArchivedAccounts}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Account">
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
CornerRadius="10"
|
||||
Padding="14,12">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="42" Height="42"
|
||||
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="18" Height="18"
|
||||
Css="{Binding Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
|
||||
<!-- Name / meta -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="3">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Type}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="·"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
<TextBlock Text="{Binding Currency}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Restore button -->
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12,8"
|
||||
Command="{Binding DataContext.UnarchiveAccountCommand, ElementName=ArchivedListView}"
|
||||
CommandParameter="{Binding .}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/rotate-ccw.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgBlue}" />
|
||||
<TextBlock Text="Restore"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/ArchivedAccountsDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ArchivedAccountsDialogViewMobile : UserControl
|
||||
{
|
||||
public ArchivedAccountsDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
522
Clario/MobileViews/AuthViewMobile.axaml
Normal file
@@ -0,0 +1,522 @@
|
||||
<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.AuthViewMobile"
|
||||
x:DataType="vm:AuthViewModel"
|
||||
Classes="mobile"
|
||||
Background="{DynamicResource BgBase}">
|
||||
<Design.DataContext>
|
||||
<vm:AuthViewModel />
|
||||
</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>
|
||||
|
||||
<!-- Tab switcher -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,28"
|
||||
IsVisible="{Binding ShowTabs}">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding isSignin}"
|
||||
CornerRadius="7"
|
||||
Padding="0,10"
|
||||
Command="{Binding SetOperationCommand}"
|
||||
CommandParameter="login">
|
||||
<TextBlock Text="Sign In"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding isCreateAccount}"
|
||||
CornerRadius="7"
|
||||
Padding="0,10"
|
||||
Command="{Binding SetOperationCommand}"
|
||||
CommandParameter="signup">
|
||||
<TextBlock Text="Create Account"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Sign In panel -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isSignin}">
|
||||
|
||||
<!-- Email -->
|
||||
<TextBlock Text="EMAIL" 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,*">
|
||||
<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>
|
||||
|
||||
<!-- Password -->
|
||||
<TextBlock Text="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,10">
|
||||
<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="••••••••"
|
||||
Text="{Binding Password}"
|
||||
PasswordChar="●"
|
||||
RevealPassword="{Binding #showPasswordLogin.IsChecked}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2"
|
||||
Name="showPasswordLogin"
|
||||
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 #showPasswordLogin.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding !#showPasswordLogin.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Panel>
|
||||
</ToggleButton>
|
||||
</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}"
|
||||
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>
|
||||
|
||||
<!-- Sign In button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,14"
|
||||
Margin="0,0,0,24"
|
||||
Command="{Binding ConfirmLoginCommand}">
|
||||
<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>
|
||||
|
||||
<!-- Create Account panel -->
|
||||
<StackPanel Spacing="0" IsVisible="{Binding isCreateAccount}">
|
||||
|
||||
<!-- First / Last name -->
|
||||
<Grid ColumnDefinitions="*,12,*" Margin="0,0,0,16">
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="FIRST NAME" Classes="label" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<TextBox Watermark="First"
|
||||
Text="{Binding FirstName}"
|
||||
Classes="ghost"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="LAST NAME" Classes="label" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<TextBox Watermark="Last"
|
||||
Classes="ghost"
|
||||
Text="{Binding LastName}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Email -->
|
||||
<TextBlock Text="EMAIL" 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,*">
|
||||
<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"
|
||||
Watermark="you@example.com"
|
||||
Classes="ghost"
|
||||
Text="{Binding Email}"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Password -->
|
||||
<TextBlock Text="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"
|
||||
Watermark="At least 8 characters"
|
||||
Classes="ghost"
|
||||
Text="{Binding Password}"
|
||||
RevealPassword="{Binding #showPasswordSignup.IsChecked}"
|
||||
PasswordChar="●"
|
||||
FontSize="14"
|
||||
Height="48"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center" />
|
||||
<ToggleButton Grid.Column="2"
|
||||
Name="showPasswordSignup"
|
||||
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 #showPasswordSignup.IsChecked}"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<Svg Path="../Assets/Icons/eye-closed.svg"
|
||||
Width="16" Height="16"
|
||||
IsVisible="{Binding !#showPasswordSignup.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,*">
|
||||
<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"
|
||||
Watermark="Repeat your password"
|
||||
PasswordChar="●"
|
||||
Text="{Binding ConfirmPassword}"
|
||||
Classes="ghost"
|
||||
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>
|
||||
|
||||
<!-- Create Account button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,14"
|
||||
Margin="0,0,0,24"
|
||||
Command="{Binding ConfirmCreateAccountCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/user-plus.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Create Account"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- 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"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/AuthViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class AuthViewMobile : UserControl
|
||||
{
|
||||
public AuthViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
393
Clario/MobileViews/BudgetFormViewMobile.axaml
Normal file
@@ -0,0 +1,393 @@
|
||||
<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:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.BudgetFormViewMobile"
|
||||
x:DataType="vm:BudgetFormViewModel"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:BudgetFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="16,16,16,0">
|
||||
|
||||
<!-- Close -->
|
||||
<Button Grid.Column="0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Width="36" Height="36"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
|
||||
<!-- Title -->
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock Text="{Binding FormTitle}"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="{Binding FormSubtitle}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Delete (edit mode only) -->
|
||||
<Button Grid.Column="2"
|
||||
Background="{DynamicResource DangerButtonBackground}"
|
||||
BorderBrush="{DynamicResource DangerButtonBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Width="36" Height="36"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsVisible="{Binding IsEditMode}"
|
||||
Command="{Binding RequestDeleteCommand}">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
</Button>
|
||||
|
||||
<!-- Placeholder to keep title centered -->
|
||||
<Border Grid.Column="2" Width="36" IsVisible="{Binding !IsEditMode}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Margin="0,16,0,0">
|
||||
<StackPanel Margin="16,0,16,16" Spacing="0">
|
||||
|
||||
<!-- Limit Amount (hero input) -->
|
||||
<TextBlock Text="LIMIT AMOUNT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,0"
|
||||
Margin="0,0,0,20"
|
||||
Height="64">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Padding="0,0,0,2" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding LimitAmount, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Height="54"
|
||||
Padding="0,0,0,2"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Category -->
|
||||
<TextBlock Text="CATEGORY" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*" Height="48">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="32" Height="32"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedCategory.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedCategory.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding SelectedCategory.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding Categories}"
|
||||
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="10,12"
|
||||
FontSize="15"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Period -->
|
||||
<TextBlock Text="PERIOD" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,20"
|
||||
Height="50">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
<Button Grid.Column="0"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsMonthly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="monthly">
|
||||
<TextBlock Text="Monthly" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsQuarterly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="quarterly">
|
||||
<TextBlock Text="Quarterly" FontSize="14" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Grid.Column="2"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsYearly}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetPeriodCommand}"
|
||||
CommandParameter="yearly">
|
||||
<TextBlock Text="Yearly" FontSize="14" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Alert Threshold -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,6">
|
||||
<TextBlock Grid.Column="0" Text="ALERT THRESHOLD" Classes="label" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding AlertThresholdLabel}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</Grid>
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="16,12"
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Spacing="6">
|
||||
<Slider Minimum="10"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
IsSnapToTickEnabled="True"
|
||||
Value="{Binding AlertThreshold}" />
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="10%"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="100%"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Rollover -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,12"
|
||||
Margin="0,0,0,20">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="8"
|
||||
Width="34" Height="34"
|
||||
Margin="0,0,12,0"
|
||||
Background="{DynamicResource IconBgPurple}">
|
||||
<Svg Path="../Assets/Icons/refresh-cw.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgPurple}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="Rollover"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="Carry unused budget to the next period"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="2"
|
||||
IsChecked="{Binding Rollover, Mode=TwoWay}"
|
||||
OnContent=""
|
||||
OffContent=""
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,8"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="16,12,16,20">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0"
|
||||
Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="{Binding SaveButtonLabel}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Delete confirm sub-modal -->
|
||||
<Grid Grid.Row="0" Grid.RowSpan="3" IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="24,0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="0">
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
</Border>
|
||||
<TextBlock Text="Delete Budget"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
<TextBlock Text="This action cannot be undone. The budget will be permanently removed."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
<Button Classes="danger"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/BudgetFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class BudgetFormViewMobile : UserControl
|
||||
{
|
||||
public BudgetFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.BudgetViewMobile"
|
||||
x:DataType="vm:BudgetViewModel"
|
||||
x:Name="budgetRoot"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:BudgetViewModel />
|
||||
@@ -16,7 +17,7 @@
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
@@ -63,7 +64,8 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Button Classes="accented"
|
||||
Padding="10,8">
|
||||
Padding="10,8"
|
||||
Command="{Binding CreateBudgetCommand}">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
@@ -72,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 -->
|
||||
@@ -93,10 +95,16 @@
|
||||
<TextBlock Text="Budgeted"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding TotalBudgeted, StringFormat='$0'}"
|
||||
FontSize="13"
|
||||
<TextBlock FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
Foreground="{DynamicResource TextPrimary}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N0}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="TotalBudgeted" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -112,10 +120,16 @@
|
||||
<TextBlock Text="Spent"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding TotalSpent, StringFormat='$0'}"
|
||||
FontSize="13"
|
||||
<TextBlock FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentRed}" />
|
||||
Foreground="{DynamicResource AccentRed}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N0}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="TotalSpent" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -140,7 +154,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Overall progress bar ──────────── -->
|
||||
<!-- Overall progress bar -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -181,7 +195,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Budget cards list ─────────────── -->
|
||||
<!-- Budget cards list -->
|
||||
<ItemsControl ItemsSource="{Binding VisibleBudgets}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
@@ -248,14 +262,16 @@
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
<!-- Menu -->
|
||||
<!-- Edit button -->
|
||||
<Button Grid.Column="3"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center">
|
||||
<Svg Path="../Assets/Icons/ellipsis.svg"
|
||||
Width="15" Height="15"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding DataContext.EditBudgetCommand, ElementName=budgetRoot}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -306,7 +322,7 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- ── Spending breakdown chart ──────── -->
|
||||
<!-- Spending breakdown chart -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -348,7 +364,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Period progress ───────────────── -->
|
||||
<!-- Period progress -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -401,7 +417,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Savings goal ──────────────────── -->
|
||||
<!-- Savings goal -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -419,7 +435,8 @@
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4">
|
||||
Padding="4"
|
||||
Command="{Binding EditSavingsGoalCommand}">
|
||||
<Svg Path="../Assets/Icons/pencil.svg" Width="14" Height="14" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -427,8 +444,15 @@
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Grid.Column="1" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N0}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="AppData.Profile.SavingsGoal" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Projected savings" FontSize="12" Foreground="{DynamicResource TextMuted}" />
|
||||
|
||||
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
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class CategoriesViewMobile : UserControl
|
||||
{
|
||||
public CategoriesViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
320
Clario/MobileViews/CategoryFormViewMobile.axaml
Normal file
@@ -0,0 +1,320 @@
|
||||
<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"
|
||||
x:Class="Clario.MobileViews.CategoryFormViewMobile"
|
||||
x:DataType="vm:CategoryFormViewModel"
|
||||
x:Name="CategoryFormRoot"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:CategoryFormViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- Bottom sheet -->
|
||||
<Border VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,32">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Handle bar -->
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,18" />
|
||||
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="10"
|
||||
Width="40" Height="40"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedIcon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding FormTitle}"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="{Binding FormSubtitle}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6"
|
||||
VerticalAlignment="Top"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="15" Height="15"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" MaxHeight="480">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Name -->
|
||||
<TextBlock Text="NAME" Classes="label" Margin="0,0,0,6" />
|
||||
<TextBox Text="{Binding Name, Mode=TwoWay}"
|
||||
Watermark="e.g. Groceries"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="14,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- Type toggle -->
|
||||
<TextBlock Text="TYPE" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Button Grid.Column="0"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsExpense}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Padding="0,10"
|
||||
Focusable="False"
|
||||
Command="{Binding SetTypeCommand}"
|
||||
CommandParameter="expense">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/arrow-up-right.svg" Width="14" Height="14" />
|
||||
<TextBlock Text="Expense" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsIncome}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
CornerRadius="7"
|
||||
Padding="0,10"
|
||||
Focusable="False"
|
||||
Command="{Binding SetTypeCommand}"
|
||||
CommandParameter="income">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/arrow-down-left.svg" Width="14" Height="14" />
|
||||
<TextBlock Text="Income" FontSize="14" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Icon picker -->
|
||||
<TextBlock Text="ICON" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="8"
|
||||
Margin="0,0,0,16">
|
||||
<ScrollViewer MaxHeight="160"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding CategoryIcons}" HorizontalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="7" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Classes="nav"
|
||||
Width="44"
|
||||
Height="44"
|
||||
Padding="0"
|
||||
Margin="2"
|
||||
CornerRadius="8"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Command="{Binding DataContext.SetIconCommand, ElementName=CategoryFormRoot}"
|
||||
CommandParameter="{Binding}">
|
||||
<Classes.accented>
|
||||
<MultiBinding Converter="{StaticResource EqualValueConverter}">
|
||||
<Binding Path="." />
|
||||
<Binding Path="DataContext.SelectedIcon" ElementName="CategoryFormRoot" />
|
||||
</MultiBinding>
|
||||
</Classes.accented>
|
||||
<Svg Path="{Binding Converter={StaticResource SvgPathFromName}}"
|
||||
Width="16" Height="16" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Color -->
|
||||
<TextBlock Text="COLOR" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="8"
|
||||
Margin="0,0,0,20">
|
||||
<ColorPicker
|
||||
Color="{Binding SelectedColor, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Height="44"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
IsAlphaEnabled="False"
|
||||
IsAlphaVisible="False"
|
||||
IsColorPaletteVisible="False"
|
||||
IsAccentColorsVisible="False" />
|
||||
</Border>
|
||||
|
||||
<!-- Error banner -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Delete (edit mode only) -->
|
||||
<Button Classes="danger"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,12"
|
||||
Margin="0,0,0,10"
|
||||
IsVisible="{Binding IsEditMode}"
|
||||
IsEnabled="{Binding CanDelete}"
|
||||
Command="{Binding RequestDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding CanDelete, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Delete Category|Min. 4 categories required'}"
|
||||
FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="{Binding SaveButtonLabel}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Delete confirm sub-modal -->
|
||||
<Grid IsVisible="{Binding ShowDeleteConfirm}">
|
||||
<Border Background="#50000000" />
|
||||
<Border HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="24,0"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="0">
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
</Border>
|
||||
<TextBlock Text="Delete Category"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
<TextBlock Text="This action cannot be undone. The category will be permanently removed."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
<Button Classes="danger"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/CategoryFormViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class CategoryFormViewMobile : UserControl
|
||||
{
|
||||
public CategoryFormViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
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
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class DashboardSkeletonViewMobile : UserControl
|
||||
{
|
||||
public DashboardSkeletonViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<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"
|
||||
@@ -7,45 +7,53 @@
|
||||
xmlns:model="clr-namespace:Clario.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:DataType="vm:DashboardViewModel"
|
||||
x:Class="Clario.MobileViews.DashboardViewMobile">
|
||||
x:Class="Clario.MobileViews.DashboardViewMobile"
|
||||
x:Name="DashboardRoot"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:DashboardViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
Background="{DynamicResource BgBase}">
|
||||
<Grid RowDefinitions="Auto,*" Background="{DynamicResource BgBase}">
|
||||
|
||||
<!-- ── Top bar ────────────────────────────── -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="16,16,16,12">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Financial Overview"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
FontSize="22" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted"
|
||||
Text="Friday, March 6, 2026"
|
||||
FontSize="12"
|
||||
Margin="0,2,0,0" />
|
||||
<TextBlock Classes="muted" Text="{Binding DateToday}" FontSize="12" Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
<!-- Settings button -->
|
||||
<Button Grid.Column="1"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="9,8"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding NavigateToSettingsCommand}">
|
||||
<Svg Path="../Assets/Icons/settings.svg" Width="16" Height="16" Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
<!-- Add transaction button -->
|
||||
<Button Grid.Column="2"
|
||||
Classes="accented"
|
||||
Padding="12,8"
|
||||
VerticalAlignment="Center">
|
||||
<Svg Path="../Assets/Icons/plus.svg"
|
||||
Width="16" Height="16"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding CreateTransactionCommand}">
|
||||
<Svg Path="../Assets/Icons/plus.svg" Width="16" Height="16"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
</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 -->
|
||||
@@ -61,22 +69,22 @@
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Padding="5">
|
||||
<Svg Path="../Assets/Icons/trending-up.svg"
|
||||
Height="12" Width="12"
|
||||
<Svg Path="../Assets/Icons/trending-up.svg" Height="12" Width="12"
|
||||
Css="{DynamicResource SvgGreen}" />
|
||||
</Border>
|
||||
<TextBlock Text="INCOME"
|
||||
Classes="label"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="INCOME" Classes="label" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding MonthlyIncome, StringFormat='$0.00'}"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock FontSize="18" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N2}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="MonthlyIncome" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<Border Classes="badge-green" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding MonthlyIncomeChangeFormatted}"
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentGreen}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -95,22 +103,22 @@
|
||||
<Border Background="{DynamicResource IconBgOrange}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Padding="5">
|
||||
<Svg Path="../Assets/Icons/trending-down.svg"
|
||||
Height="12" Width="12"
|
||||
<Svg Path="../Assets/Icons/trending-down.svg" Height="12" Width="12"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
</Border>
|
||||
<TextBlock Text="EXPENSES"
|
||||
Classes="label"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="EXPENSES" Classes="label" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding MonthlyExpenses, StringFormat='$0.00'}"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock FontSize="18" FontWeight="Bold" Foreground="{DynamicResource TextPrimary}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N2}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="MonthlyExpenses" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<Border Classes="badge-red" HorizontalAlignment="Left">
|
||||
<TextBlock Text="{Binding MonthlyExpenseChangeFormatted}"
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentRed}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -129,13 +137,10 @@
|
||||
<Border Background="{DynamicResource IconBgPurple}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Padding="5">
|
||||
<Svg Path="../Assets/Icons/landmark.svg"
|
||||
Height="12" Width="12"
|
||||
<Svg Path="../Assets/Icons/landmark.svg" Height="12" Width="12"
|
||||
Css="{DynamicResource SvgPurple}" />
|
||||
</Border>
|
||||
<TextBlock Text="SAVINGS RATE"
|
||||
Classes="label"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="SAVINGS RATE" Classes="label" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<ProgressBar Grid.Column="0"
|
||||
@@ -145,8 +150,7 @@
|
||||
Value="{Binding MonthlyExpenses}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock.Text>
|
||||
@@ -160,7 +164,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Spending by category chart ────── -->
|
||||
<!-- Spending by category chart -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -170,79 +174,104 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Spending by Category"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="March 2026" FontSize="11" />
|
||||
<TextBlock Classes="muted" Text="{Binding SelectedChartTimPeriodSubTitle}" FontSize="11" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedIndex="0"
|
||||
ItemsSource="{Binding ChartTimePeriods}"
|
||||
SelectedItem="{Binding SelectedChartTimePeriod}"
|
||||
Background="{DynamicResource BgHover}"
|
||||
Foreground="{DynamicResource TextSecondary}"
|
||||
BorderBrush="{DynamicResource BorderAccent}"
|
||||
CornerRadius="{StaticResource RadiusIcon}"
|
||||
Padding="8,5"
|
||||
FontSize="12" />
|
||||
Padding="8,5" FontSize="12" />
|
||||
</Grid>
|
||||
|
||||
<lvc:CartesianChart Series="{Binding SpendingByCategoryChartSeries}"
|
||||
Height="180"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
LegendPosition="Hidden"
|
||||
TooltipPosition="Hidden">
|
||||
<lvc:CartesianChart.XAxes>
|
||||
<lvc:XamlAxis IsVisible="False" />
|
||||
</lvc:CartesianChart.XAxes>
|
||||
<lvc:CartesianChart.YAxes>
|
||||
<lvc:XamlLogarithmicAxis LogBase="10" IsVisible="False" MinLimit="1" />
|
||||
</lvc:CartesianChart.YAxes>
|
||||
</lvc:CartesianChart>
|
||||
<Panel>
|
||||
<!-- Chart + labels (when there is data) -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding HasSpendingData}">
|
||||
<lvc:CartesianChart Series="{Binding SpendingByCategoryChartSeries}"
|
||||
Height="180"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
LegendPosition="Hidden"
|
||||
TooltipPosition="Hidden"
|
||||
ZoomMode="None"
|
||||
AnimationsSpeed="00:00:00.1">
|
||||
<lvc:CartesianChart.XAxes>
|
||||
<lvc:XamlAxis IsVisible="False" />
|
||||
</lvc:CartesianChart.XAxes>
|
||||
<lvc:CartesianChart.YAxes>
|
||||
<lvc:XamlLogarithmicAxis LogBase="10" IsVisible="False" MinLimit="1" />
|
||||
</lvc:CartesianChart.YAxes>
|
||||
</lvc:CartesianChart>
|
||||
|
||||
<!-- Category labels -->
|
||||
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ColumnChartData">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!-- Category labels -->
|
||||
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ColumnChartData">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border HorizontalAlignment="Stretch"
|
||||
Height="1"
|
||||
Background="{DynamicResource BorderSubtle}" />
|
||||
<Border HorizontalAlignment="Stretch" Height="1"
|
||||
Background="{DynamicResource BorderSubtle}" />
|
||||
|
||||
<!-- Category amounts -->
|
||||
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ColumnChartData">
|
||||
<TextBlock Text="{Binding Values, Converter={StaticResource FirstValueConverter}, StringFormat='$0,00'}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="10"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{Binding Fill, Converter={StaticResource SkPaintToBrushConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!-- Category amounts -->
|
||||
<ItemsControl ItemsSource="{Binding SpendingByCategoryChartData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ColumnChartData">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
FontSize="10" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Foreground="{Binding Fill, Converter={StaticResource SkPaintToBrushConverter}}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N0}">
|
||||
<Binding Path="DataContext.PrimarySymbol" ElementName="DashboardRoot" />
|
||||
<Binding Path="Values" Converter="{StaticResource FirstValueConverter}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Spacing="10" Margin="0,40"
|
||||
IsVisible="{Binding !HasSpendingData}">
|
||||
<Svg Path="../Assets/Icons/chart-column.svg" Css="{DynamicResource SvgDisabled}"
|
||||
Height="36" Width="36" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="No spending data"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Add expense transactions to see your spending breakdown."
|
||||
FontSize="12" Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap"
|
||||
TextAlignment="Center" MaxWidth="220" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── Recent transactions ───────────── -->
|
||||
<!-- Recent transactions -->
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
@@ -252,148 +281,193 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Recent Transactions"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="Last 5 transactions" FontSize="11" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource AccentBlue}"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
Padding="0"
|
||||
Content="View all →"
|
||||
VerticalAlignment="Center"
|
||||
BorderThickness="0" FontSize="12" Padding="0"
|
||||
Content="View all →" VerticalAlignment="Center"
|
||||
Command="{Binding ViewAllTransactionsCommand}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Transaction rows -->
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10">
|
||||
<ItemsControl ItemsSource="{Binding RecentTransactions}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
CornerRadius="10">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Transaction">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
<Panel>
|
||||
<!-- Transaction rows -->
|
||||
<Border Background="{DynamicResource BorderSubtle}" CornerRadius="10"
|
||||
IsVisible="{Binding HasTransactionData}">
|
||||
<ItemsControl ItemsSource="{Binding RecentTransactions}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Margin="12,10">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="9"
|
||||
Width="36" Height="36"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Height="16" Width="16"
|
||||
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Category.Name}" Classes="muted" FontSize="11" />
|
||||
<TextBlock Text="·" Classes="muted" FontSize="11" />
|
||||
<TextBlock
|
||||
Text="{Binding Date, Converter={StaticResource DateFormatConverter}, ConverterParameter='MMM d'}"
|
||||
Classes="muted" FontSize="11" />
|
||||
CornerRadius="10">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Transaction">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
Margin="12,10">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="9" Width="36" Height="36"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Height="16" Width="16"
|
||||
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Category.Name}" Classes="muted" FontSize="11" />
|
||||
<TextBlock Text="·" Classes="muted" FontSize="11" />
|
||||
<TextBlock
|
||||
Text="{Binding Date, Converter={StaticResource DateFormatConverter}, ConverterParameter='MMM d'}"
|
||||
Classes="muted" FontSize="11" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource AmountSignConverter}">
|
||||
<Binding Path="Amount" />
|
||||
<Binding Path="Type" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource AmountSignConverter}">
|
||||
<Binding Path="Amount" />
|
||||
<Binding Path="Type" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Spacing="10" Margin="0,32"
|
||||
IsVisible="{Binding !HasTransactionData}">
|
||||
<Svg Path="../Assets/Icons/receipt.svg" Css="{DynamicResource SvgDisabled}"
|
||||
Height="36" Width="36" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="No transactions yet"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Add your first transaction to get started."
|
||||
FontSize="12" Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap"
|
||||
TextAlignment="Center" MaxWidth="220" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</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>
|
||||
<ItemsControl ItemsSource="{Binding BudgetsTrackerData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="14" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Budget">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Height="13" Width="13"
|
||||
Css="{DynamicResource SvgSecondary}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding Category.Name}"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
</StackPanel>
|
||||
<Panel Grid.Column="1">
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding !IsOverBudget}">
|
||||
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<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}"
|
||||
IsVisible="{Binding HasBudgetData}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="14" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="model:Budget">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Height="13" Width="13"
|
||||
Css="{DynamicResource SvgSecondary}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding Category.Name}"
|
||||
FontSize="13" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
|
||||
<TextBlock Text="{Binding Spent, StringFormat='$0'}" FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text="{Binding LimitAmount, StringFormat='$0'}" FontSize="11"
|
||||
Foreground="{DynamicResource AccentRed}" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
<ProgressBar Classes.green="{Binding IsOnTrack}"
|
||||
Classes.yellow="{Binding IsWarning}"
|
||||
Classes.red="{Binding IsOverBudget}"
|
||||
Minimum="0"
|
||||
Value="{Binding Spent}"
|
||||
Maximum="{Binding LimitAmount}"
|
||||
Height="5" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Panel Grid.Column="1">
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding !IsOverBudget}">
|
||||
<TextBlock Text="{Binding SpentFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="{Binding LimitFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsOverBudget}">
|
||||
<TextBlock Text="{Binding SpentFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text=" / " FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
<TextBlock Text="{Binding LimitFormatted}"
|
||||
FontSize="11" Foreground="{DynamicResource AccentRed}" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
<ProgressBar Classes.green="{Binding IsOnTrack}"
|
||||
Classes.yellow="{Binding IsWarning}"
|
||||
Classes.red="{Binding IsOverBudget}"
|
||||
Minimum="0" Value="{Binding Spent}"
|
||||
Maximum="{Binding LimitAmount}" Height="5" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Spacing="10" Margin="0,20"
|
||||
IsVisible="{Binding !HasBudgetData}">
|
||||
<Svg Path="../Assets/Icons/wallet.svg" Css="{DynamicResource SvgDisabled}"
|
||||
Height="32" Width="32" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="No budgets set"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
HorizontalAlignment="Center" />
|
||||
<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"
|
||||
@@ -402,8 +476,7 @@
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Accounts"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Classes="muted" Text="{Binding AccountsSubtitle}" FontSize="11" />
|
||||
</StackPanel>
|
||||
@@ -423,8 +496,7 @@
|
||||
Background="{DynamicResource BgBase}"
|
||||
Margin="12,10">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="9"
|
||||
Width="34" Height="34"
|
||||
CornerRadius="9" Width="34" Height="34"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
@@ -437,17 +509,14 @@
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextSecondary}" />
|
||||
<TextBlock Text="{Binding Mask, Converter={StaticResource MaskToStringConverter}}"
|
||||
FontSize="11"
|
||||
Classes="muted" />
|
||||
FontSize="11" Classes="muted" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding CurrentBalance, StringFormat='$0'}"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Text="{Binding CurrentBalanceFormatted}"
|
||||
FontSize="13" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
@@ -459,17 +528,20 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Total Balance"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding TotalNetworth, StringFormat=$0}"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
FontSize="17" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0}{1:N2}">
|
||||
<Binding Path="PrimarySymbol" />
|
||||
<Binding Path="TotalNetworth" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -477,4 +549,4 @@
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
||||
454
Clario/MobileViews/DeleteAccountDialogViewMobile.axaml
Normal file
@@ -0,0 +1,454 @@
|
||||
<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"
|
||||
x:Class="Clario.MobileViews.DeleteAccountDialogViewMobile"
|
||||
x:DataType="vm:DeleteAccountDialogViewModel"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:DeleteAccountDialogViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- STEP 1 — Simple confirm (no transactions) -->
|
||||
<Border IsVisible="{Binding IsSimpleConfirmStep}"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,40">
|
||||
<StackPanel Spacing="0">
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Delete Account"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- Account badge -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,14">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border CornerRadius="7" Width="26" Height="26" VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="This account has no transactions. It will be permanently deleted and cannot be recovered."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="danger"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="Delete Account" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- STEP 1B — Has transactions warning -->
|
||||
<Border IsVisible="{Binding IsHasTransactionsStep}"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,40">
|
||||
<StackPanel Spacing="0">
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/triangle-alert.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgYellow}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Account Has Transactions"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- Account badge -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,14">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border CornerRadius="7" Width="26" Height="26" VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBlock Text="{Binding Account.TransactionsCount}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentYellow}" />
|
||||
<TextBlock Text="transactions"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AccentYellow}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock FontSize="13"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24">
|
||||
<Run Text="This account has linked transactions. Before deleting, you must migrate them to another account." />
|
||||
</TextBlock>
|
||||
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding GoToMigrateStepCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/arrow-right-left.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #0D0F14; }" />
|
||||
<TextBlock Text="Migrate & Delete"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- STEP 2 — Pick target account + confirm -->
|
||||
<Border IsVisible="{Binding IsMigrateStep}"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,40">
|
||||
<StackPanel Spacing="0">
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Header -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||
<Button Grid.Column="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding BackToWarningCommand}">
|
||||
<Svg Path="../Assets/Icons/arrow-left.svg"
|
||||
Width="16" Height="16"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="Migrate Transactions"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="Choose where to move the transactions"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding CancelCommand}">
|
||||
<Svg Path="../Assets/Icons/x.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- From → To visual -->
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,12"
|
||||
Margin="0,0,0,18">
|
||||
<Grid ColumnDefinitions="*,Auto,*">
|
||||
<StackPanel Grid.Column="0" Spacing="4" HorizontalAlignment="Center">
|
||||
<TextBlock Text="FROM" Classes="label" HorizontalAlignment="Center" />
|
||||
<Border CornerRadius="8" Width="38" Height="38" HorizontalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Account.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Account.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Account.Name}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock FontSize="10" Foreground="{DynamicResource AccentRed}" HorizontalAlignment="Center">
|
||||
<Run Text="{Binding Account.TransactionsCount}" />
|
||||
<Run Text=" transactions" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Svg Grid.Column="1"
|
||||
Path="../Assets/Icons/arrow-right.svg"
|
||||
Width="18" Height="18"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0" />
|
||||
|
||||
<StackPanel Grid.Column="2" Spacing="4" HorizontalAlignment="Center">
|
||||
<TextBlock Text="TO" Classes="label" HorizontalAlignment="Center" />
|
||||
<Border CornerRadius="8" Width="38" Height="38" HorizontalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding TargetAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding TargetAccount.Name}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center" />
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
CornerRadius="6"
|
||||
Padding="6,2"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text="Target"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
HorizontalAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Target account selector -->
|
||||
<TextBlock Text="SELECT TARGET ACCOUNT" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding TargetAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="14" Height="14"
|
||||
Css="{Binding TargetAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding AvailableAccounts}"
|
||||
SelectedItem="{Binding TargetAccount, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="8,13"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Warning info -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="8">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/info.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgYellow}"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,1,0,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentYellow}"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="All transactions will be moved to" />
|
||||
<Run Text=" " />
|
||||
<Run Text="{Binding TargetAccount.Name}" FontWeight="SemiBold" />
|
||||
<Run Text=". Balances will be recalculated. This cannot be undone." />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="danger"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding CanMigrateAndDelete}"
|
||||
Command="{Binding MigrateAndDeleteCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/trash-2.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="Migrate & Delete" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/DeleteAccountDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class DeleteAccountDialogViewMobile : UserControl
|
||||
{
|
||||
public DeleteAccountDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
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:views="clr-namespace:Clario.Views"
|
||||
xmlns:mobileViews="clr-namespace:Clario.MobileViews"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Clario.MobileViews.MainViewMobile"
|
||||
@@ -19,11 +18,23 @@
|
||||
DataContext="{Binding TransactionFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsTransactionFormVisible,ElementName=MainControl}">
|
||||
</mobileViews:TransactionFormViewMobile>
|
||||
<!-- ── Content area ──────────────────────── -->
|
||||
<mobileViews:AccountFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
|
||||
DataContext="{Binding AccountFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsAccountFormVisible, ElementName=MainControl}" />
|
||||
<mobileViews:BudgetFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
|
||||
DataContext="{Binding BudgetFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsBudgetFormVisible, ElementName=MainControl}" />
|
||||
<mobileViews:CategoryFormViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
|
||||
DataContext="{Binding CategoryFormViewModel}"
|
||||
IsVisible="{Binding DataContext.IsCategoryFormVisible, ElementName=MainControl}" />
|
||||
<mobileViews:SetSavingsGoalDialogViewMobile Grid.Row="0" Grid.RowSpan="2" ZIndex="3"
|
||||
DataContext="{Binding SetSavingsGoalDialogViewModel}"
|
||||
IsVisible="{Binding DataContext.IsSavingsGoalDialogVisible, ElementName=MainControl}" />
|
||||
<!-- 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}"
|
||||
@@ -98,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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class ResetPasswordViewMobile : UserControl
|
||||
{
|
||||
public ResetPasswordViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
150
Clario/MobileViews/SetSavingsGoalDialogViewMobile.axaml
Normal file
@@ -0,0 +1,150 @@
|
||||
<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:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Clario.MobileViews.SetSavingsGoalDialogViewMobile"
|
||||
x:DataType="vm:SetSavingsGoalDialogViewModel"
|
||||
Classes="mobile">
|
||||
<Design.DataContext>
|
||||
<vm:SetSavingsGoalDialogViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Border Background="#70000000" />
|
||||
|
||||
<!-- Bottom sheet -->
|
||||
<Border VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource AccentBlue}"
|
||||
BorderThickness="0,1,0,0"
|
||||
CornerRadius="18,18,0,0"
|
||||
Padding="20,20,20,40">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Handle bar -->
|
||||
<Border Width="36" Height="4"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource BorderSubtle}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="{DynamicResource IconBgBlue}"
|
||||
CornerRadius="14"
|
||||
Width="54" Height="54"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16">
|
||||
<Svg Path="../Assets/Icons/target.svg"
|
||||
Width="22" Height="22"
|
||||
Css="{DynamicResource SvgBlue}" />
|
||||
</Border>
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Text="Set Savings Goal"
|
||||
FontSize="17"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextBlock Text="Set a monthly savings target to track your progress on the budget page."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,24" />
|
||||
|
||||
<!-- Amount input -->
|
||||
<TextBlock Text="MONTHLY GOAL" Classes="label" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Margin="0,0,0,6">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="16,0,0,0" />
|
||||
<TextBox Grid.Column="1"
|
||||
Classes="ghost"
|
||||
Text="{Binding GoalInput, Mode=TwoWay}"
|
||||
Watermark="0.00"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Padding="8,14">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Set to 0 to remove the goal"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextDisabled}"
|
||||
Margin="0,0,0,20" />
|
||||
|
||||
<!-- Error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding HasError}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Actions -->
|
||||
<UniformGrid Rows="1">
|
||||
<Button Classes="base"
|
||||
Margin="0,0,6,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
FontSize="14"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Classes="accented"
|
||||
Margin="6,0,0,0"
|
||||
Padding="0,13"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Command="{Binding SaveCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/check.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgBase}" />
|
||||
<TextBlock Text="Save Goal"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/SetSavingsGoalDialogViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class SetSavingsGoalDialogViewMobile : UserControl
|
||||
{
|
||||
public SetSavingsGoalDialogViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
486
Clario/MobileViews/SettingsViewMobile.axaml
Normal file
@@ -0,0 +1,486 @@
|
||||
<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.SettingsViewMobile"
|
||||
x:DataType="vm:SettingsViewModel"
|
||||
Classes="mobile"
|
||||
Background="{DynamicResource BgBase}">
|
||||
<Design.DataContext>
|
||||
<vm:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<!-- Top bar -->
|
||||
<Grid Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="16,16,16,12">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Settings"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,0,16,32" Spacing="0">
|
||||
|
||||
<!-- Global success / error banners -->
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,12"
|
||||
IsVisible="{Binding HasSuccess}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/circle-check.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgGreen}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding SuccessMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,12"
|
||||
IsVisible="{Binding HasError}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Svg Grid.Column="0"
|
||||
Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="14" Height="14"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding ErrorMessage}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
SECTION: Profile
|
||||
-->
|
||||
<TextBlock Text="PROFILE" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="16"
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Spacing="16">
|
||||
|
||||
<!-- Avatar row -->
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Panel Grid.Column="0"
|
||||
Width="64" Height="64"
|
||||
Margin="0,0,16,0">
|
||||
<Border CornerRadius="32"
|
||||
ClipToBounds="True"
|
||||
Width="64" Height="64"
|
||||
IsVisible="{Binding HasAvatar}">
|
||||
<Image Source="{Binding AvatarImage}" Stretch="UniformToFill" />
|
||||
</Border>
|
||||
<Border CornerRadius="32"
|
||||
Width="64" Height="64"
|
||||
Background="{DynamicResource BorderAccent}"
|
||||
IsVisible="{Binding !HasAvatar}">
|
||||
<TextBlock Text="{Binding DisplayName[0]}"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource AccentBlue}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Border CornerRadius="32"
|
||||
Width="64" Height="64"
|
||||
Background="#80000000"
|
||||
IsVisible="{Binding IsUploadingAvatar}">
|
||||
<TextBlock Text="..."
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button Classes="base"
|
||||
Padding="14,8"
|
||||
IsEnabled="{Binding !IsUploadingAvatar}"
|
||||
Command="{Binding UploadAvatarCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Svg Path="../Assets/Icons/upload.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Upload Photo" FontSize="13" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
IsVisible="{Binding HasAvatar}"
|
||||
IsEnabled="{Binding !IsUploadingAvatar}"
|
||||
Command="{Binding RemoveAvatarCommand}"
|
||||
Content="Remove photo" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Display name -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="DISPLAY NAME" Classes="label" />
|
||||
<TextBox Text="{Binding DisplayName, Mode=TwoWay}"
|
||||
Watermark="Your name"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Theme -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="THEME" Classes="label" />
|
||||
<ComboBox ItemsSource="{Binding ThemeLabels}"
|
||||
SelectedIndex="{Binding SelectedThemeIndex, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="12,10"
|
||||
FontSize="14" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Language -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="LANGUAGE" Classes="label" />
|
||||
<ComboBox ItemsSource="{Binding LanguageLabels}"
|
||||
SelectedIndex="{Binding SelectedLanguageIndex, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="12,10"
|
||||
FontSize="14" />
|
||||
</StackPanel>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Save button -->
|
||||
<Button Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,12"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding SaveProfileCommand}">
|
||||
<TextBlock Text="{Binding IsSaving, Converter={StaticResource BoolToStringConverter}, ConverterParameter='Saving...|Save Changes'}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
SECTION: Account & Security
|
||||
-->
|
||||
<TextBlock Text="ACCOUNT & SECURITY" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="0"
|
||||
Margin="0,0,0,20">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Email row -->
|
||||
<Border BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="16,0">
|
||||
<Panel>
|
||||
<!-- Display row -->
|
||||
<Grid IsVisible="{Binding !IsChangingEmail}"
|
||||
ColumnDefinitions="*,Auto"
|
||||
MinHeight="56">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="EMAIL ADDRESS" Classes="label" />
|
||||
<TextBlock Text="{Binding MaskedEmail}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextPrimary}"
|
||||
FontWeight="SemiBold" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding StartChangeEmailCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Change"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Change email form -->
|
||||
<StackPanel IsVisible="{Binding IsChangingEmail}"
|
||||
Spacing="12"
|
||||
Margin="0,16,0,16">
|
||||
<TextBlock Text="CHANGE EMAIL ADDRESS" Classes="label" />
|
||||
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasEmailSuccess}">
|
||||
<TextBlock Text="{Binding EmailSuccessMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasEmailError}">
|
||||
<TextBlock Text="{Binding EmailErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="NEW EMAIL" Classes="label" />
|
||||
<TextBox Text="{Binding NewEmail, Mode=TwoWay}"
|
||||
Watermark="new@email.com"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="CONFIRM WITH PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding EmailConfirmPassword, Mode=TwoWay}"
|
||||
Watermark="Current password"
|
||||
PasswordChar="•"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||
<Button Grid.Column="0"
|
||||
Classes="base"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,11"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelChangeEmailCommand}" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,11"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding ConfirmChangeEmailCommand}">
|
||||
<TextBlock Text="Update Email"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<!-- Password row -->
|
||||
<Border Padding="16,0">
|
||||
<Panel>
|
||||
<!-- Display row -->
|
||||
<Grid IsVisible="{Binding !IsChangingPassword}"
|
||||
ColumnDefinitions="*,Auto"
|
||||
MinHeight="56">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="PASSWORD" Classes="label" />
|
||||
<TextBlock Text="••••••••••••"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
LetterSpacing="2" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding StartChangePasswordCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Svg Path="../Assets/Icons/pencil.svg"
|
||||
Width="13" Height="13"
|
||||
Css="{DynamicResource SvgMuted}" />
|
||||
<TextBlock Text="Change"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Change password form -->
|
||||
<StackPanel IsVisible="{Binding IsChangingPassword}"
|
||||
Spacing="12"
|
||||
Margin="0,16,0,16">
|
||||
<TextBlock Text="CHANGE PASSWORD" Classes="label" />
|
||||
|
||||
<Border Background="{DynamicResource IconBgGreen}"
|
||||
BorderBrush="{DynamicResource AccentGreen}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasPasswordSuccess}">
|
||||
<TextBlock Text="{Binding PasswordSuccessMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentGreen}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
IsVisible="{Binding HasPasswordError}">
|
||||
<TextBlock Text="{Binding PasswordErrorMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="CURRENT PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding CurrentPassword, Mode=TwoWay}"
|
||||
Watermark="Enter current password"
|
||||
PasswordChar="•"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="NEW PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding NewPassword, Mode=TwoWay}"
|
||||
Watermark="Min. 8 characters"
|
||||
PasswordChar="•"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="CONFIRM NEW PASSWORD" Classes="label" />
|
||||
<TextBox Text="{Binding ConfirmNewPassword, Mode=TwoWay}"
|
||||
Watermark="Repeat new password"
|
||||
PasswordChar="•"
|
||||
FontSize="14"
|
||||
Height="44"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||
<Button Grid.Column="0"
|
||||
Classes="base"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,11"
|
||||
FontSize="13"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelChangePasswordCommand}" />
|
||||
<Button Grid.Column="1"
|
||||
Classes="accented"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Padding="0,11"
|
||||
IsEnabled="{Binding !IsSaving}"
|
||||
Command="{Binding ConfirmChangePasswordCommand}">
|
||||
<TextBlock Text="Update Password"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BgBase}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
SECTION: Session
|
||||
-->
|
||||
<TextBlock Text="SESSION" Classes="label" Margin="0,0,0,10" />
|
||||
<Border Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="Sign Out"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<TextBlock Text="You will be returned to the login screen."
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Classes="danger"
|
||||
Padding="14,10"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding SignOutCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<Svg Path="../Assets/Icons/log-out.svg" Width="13" Height="13" Css="{DynamicResource SvgRed}" />
|
||||
<TextBlock Text="Sign Out" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Clario/MobileViews/SettingsViewMobile.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Clario.MobileViews;
|
||||
|
||||
public partial class SettingsViewMobile : UserControl
|
||||
{
|
||||
public SettingsViewMobile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:Clario.ViewModels"
|
||||
xmlns:cc="clr-namespace:Clario.CustomControls"
|
||||
xmlns:behaviors="clr-namespace:Clario.Behaviors"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
|
||||
x:Class="Clario.MobileViews.TransactionFormViewMobile"
|
||||
x:DataType="vm:TransactionFormViewModel"
|
||||
@@ -15,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">
|
||||
@@ -53,8 +54,8 @@
|
||||
|
||||
<!-- Delete (edit mode only) -->
|
||||
<Button Grid.Column="2"
|
||||
Background="#1A0808"
|
||||
BorderBrush="#3A1515"
|
||||
Background="{DynamicResource DangerButtonBackground}"
|
||||
BorderBrush="{DynamicResource DangerButtonBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Width="36" Height="36"
|
||||
@@ -74,7 +75,7 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Scrollable form ────────────────────── -->
|
||||
<!-- Scrollable form -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
@@ -88,7 +89,7 @@
|
||||
Padding="3"
|
||||
Margin="0,0,0,20"
|
||||
Height="50">
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
<!-- Expense -->
|
||||
<Button Grid.Column="0"
|
||||
Classes="nav"
|
||||
@@ -101,11 +102,11 @@
|
||||
Focusable="False"
|
||||
Command="{Binding SetTypeCommand}"
|
||||
CommandParameter="expense">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Svg Path="../Assets/Icons/arrow-up-right.svg"
|
||||
Width="16" Height="16" />
|
||||
Width="14" Height="14" />
|
||||
<TextBlock Text="Expense"
|
||||
FontSize="16"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
@@ -122,18 +123,38 @@
|
||||
Focusable="False"
|
||||
Command="{Binding SetTypeCommand}"
|
||||
CommandParameter="income">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Svg Path="../Assets/Icons/arrow-down-left.svg"
|
||||
Width="16" Height="16" />
|
||||
Width="14" Height="14" />
|
||||
<TextBlock Text="Income"
|
||||
FontSize="16"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<!-- Transfer -->
|
||||
<Button Grid.Column="2"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding IsTransfer}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="7"
|
||||
Padding="0,8"
|
||||
Focusable="False"
|
||||
Command="{Binding SetTypeCommand}"
|
||||
CommandParameter="transfer">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Svg Path="../Assets/Icons/arrow-right-left.svg"
|
||||
Width="14" Height="14" />
|
||||
<TextBlock Text="Transfer"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Amount ──────────────────────── -->
|
||||
<!-- Amount -->
|
||||
<TextBlock Text="AMOUNT" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -144,7 +165,7 @@
|
||||
Height="64">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="$"
|
||||
Text="{Binding CurrencySymbol}"
|
||||
FontSize="32"
|
||||
FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextMuted}"
|
||||
@@ -161,6 +182,9 @@
|
||||
Height="54"
|
||||
Padding="0 0 0 2"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding Currency}"
|
||||
@@ -171,18 +195,21 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Description ─────────────────── -->
|
||||
<TextBlock Text="DESCRIPTION" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<!-- 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}"
|
||||
Watermark="e.g. Grocery Shopping"
|
||||
FontSize="16"
|
||||
Height="48"
|
||||
Padding="12,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}" />
|
||||
|
||||
<!-- ── Category + Account ──────────── -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16">
|
||||
<!-- Category + Account (income/expense) -->
|
||||
<Grid ColumnDefinitions="*,14,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding !IsTransfer}">
|
||||
|
||||
<!-- Category -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
@@ -260,7 +287,144 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── Date ────────────────────────── -->
|
||||
<!-- From + To accounts (transfer) -->
|
||||
<Grid ColumnDefinitions="*,Auto,*" Margin="0,0,0,16"
|
||||
IsVisible="{Binding IsTransfer}">
|
||||
<!-- From Account -->
|
||||
<StackPanel Grid.Column="0" Spacing="6">
|
||||
<TextBlock Text="FROM ACCOUNT" Classes="label" FontSize="12" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<Grid ColumnDefinitions="Auto,*" Height="42">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding SelectedAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding Accounts}"
|
||||
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6,8"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<!-- Arrow -->
|
||||
<Svg Grid.Column="1"
|
||||
Path="../Assets/Icons/arrow-right.svg"
|
||||
Width="14" Height="14"
|
||||
Css="{DynamicResource SvgMuted}"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="6,0,6,12" />
|
||||
<!-- To Account -->
|
||||
<StackPanel Grid.Column="2" Spacing="6">
|
||||
<TextBlock Text="TO ACCOUNT" Classes="label" FontSize="12" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}">
|
||||
<Grid ColumnDefinitions="Auto,*" Height="42">
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="7"
|
||||
Width="30" Height="30"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding SelectedToAccount.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="13" Height="13"
|
||||
Css="{Binding SelectedToAccount.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding Accounts}"
|
||||
SelectedItem="{Binding SelectedToAccount, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="6,8"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Exchange Rate -->
|
||||
<Border IsVisible="{Binding ShowExchangeRateField}"
|
||||
Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="14,10"
|
||||
Margin="0,0,0,16">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="EXCHANGE RATE"
|
||||
Classes="label"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="{Binding IsFetchingRate}"
|
||||
VerticalAlignment="Center">
|
||||
<Border Width="8" Height="8"
|
||||
CornerRadius="4"
|
||||
Background="{DynamicResource AccentBlue}"
|
||||
Opacity="0.7" />
|
||||
<TextBlock Text="Fetching rate..."
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding ExchangeRateLabel}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<Border IsVisible="{Binding IsFetchingRate}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Height="36"
|
||||
Opacity="0.5" />
|
||||
<Border IsVisible="{Binding !IsFetchingRate}"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="12,0">
|
||||
<TextBox Classes="ghost"
|
||||
Text="{Binding ExchangeRate, Mode=TwoWay}"
|
||||
Watermark="0.00000"
|
||||
FontSize="13"
|
||||
Height="36"
|
||||
Padding="0"
|
||||
VerticalContentAlignment="Center">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:NumericInputBehavior />
|
||||
</Interaction.Behaviors>
|
||||
</TextBox>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Date -->
|
||||
<TextBlock Text="DATE" Classes="label" FontSize="14" Margin="0,0,0,6" />
|
||||
<Border Background="{DynamicResource BgBase}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -292,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..."
|
||||
@@ -302,7 +466,47 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- ── Validation error ─────────────── -->
|
||||
<!-- Budget approaching warning -->
|
||||
<Border Background="{DynamicResource BadgeBgYellow}"
|
||||
BorderBrush="{DynamicResource AccentYellow}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,8,0,8"
|
||||
IsVisible="{Binding HasBudgetApproachingWarning}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/triangle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #F5C842; }" />
|
||||
<TextBlock Text="{Binding BudgetWarningMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentYellow}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Budget over-limit warning -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="12,8"
|
||||
Margin="0,8,0,8"
|
||||
IsVisible="{Binding BudgetWarningIsOverBudget}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Svg Path="../Assets/Icons/circle-alert.svg"
|
||||
Width="13" Height="13"
|
||||
Css="path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #FF5E5E; }" />
|
||||
<TextBlock Text="{Binding BudgetWarningMessage}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentRed}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Validation error -->
|
||||
<Border Background="{DynamicResource BadgeBgRed}"
|
||||
BorderBrush="{DynamicResource AccentRed}"
|
||||
BorderThickness="1"
|
||||
@@ -324,7 +528,7 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── Bottom action bar ──────────────────── -->
|
||||
<!-- Bottom action bar -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource BgSurface}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
@@ -370,12 +574,11 @@
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
Padding="28"
|
||||
Width="340"
|
||||
BoxShadow="0 24 72 0 #60000000">
|
||||
Width="340">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Background="#2A0D0D"
|
||||
<Border Background="{DynamicResource IconBgRed}"
|
||||
CornerRadius="14"
|
||||
Width="52" Height="52"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -416,7 +619,7 @@
|
||||
Padding="0,11"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="#FF5E5E"
|
||||
Background="{DynamicResource AccentRed}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Command="{Binding ConfirmDeleteCommand}">
|
||||
@@ -427,7 +630,7 @@
|
||||
<TextBlock Text="Delete"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFFF"
|
||||
Foreground="{DynamicResource BgBase}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Clario.MobileViews.TransactionsViewMobile"
|
||||
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">
|
||||
@@ -37,8 +41,7 @@
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="20"
|
||||
Width="300"
|
||||
BoxShadow="0 8 32 0 #3C000000">
|
||||
Width="300">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<TextBlock Text="FILTERS"
|
||||
@@ -71,7 +74,7 @@
|
||||
CornerRadius="{DynamicResource RadiusControl}"
|
||||
Padding="3"
|
||||
Margin="0,0,0,14">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
<Button Grid.Column="0"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding FilterTypeAll}"
|
||||
@@ -108,6 +111,18 @@
|
||||
CommandParameter="expense">
|
||||
<TextBlock Text="Expense" FontSize="12" HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Grid.Column="3"
|
||||
Classes="nav"
|
||||
Classes.accented="{Binding FilterTypeTransfer}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Focusable="False"
|
||||
CornerRadius="7"
|
||||
Padding="0,6"
|
||||
Command="{Binding SetTransactionTypeCommand}"
|
||||
CommandParameter="transfer">
|
||||
<TextBlock Text="Transfer" FontSize="12" HorizontalAlignment="Center" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -157,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"
|
||||
@@ -176,7 +192,7 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── Summary strip ─────────────────────── -->
|
||||
<!-- Summary strip -->
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="*,*,*"
|
||||
Margin="16,12,16,0">
|
||||
@@ -259,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 />
|
||||
@@ -282,74 +298,119 @@
|
||||
Classes="label" />
|
||||
</Border>
|
||||
|
||||
<!-- Transaction row -->
|
||||
<Border Padding="16,12"
|
||||
IsVisible="{Binding !GroupHeader}"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<!-- Transaction row (tappable to edit) -->
|
||||
<Button IsVisible="{Binding !GroupHeader}"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Focusable="False"
|
||||
Command="{Binding DataContext.EditTransactionCommand, ElementName=transactionsRoot}"
|
||||
CommandParameter="{Binding .}">
|
||||
<Border Padding="16,12"
|
||||
BorderBrush="{DynamicResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="{DynamicResource RadiusIcon}"
|
||||
Width="38" Height="38"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<!-- Icon -->
|
||||
<Panel Grid.Column="0"
|
||||
Width="38" Height="38"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0">
|
||||
<!-- Category icon -->
|
||||
<Border CornerRadius="{DynamicResource RadiusIcon}"
|
||||
Width="38" Height="38"
|
||||
IsVisible="{Binding !IsTransfer}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush
|
||||
Color="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=color}"
|
||||
Opacity="0.15" />
|
||||
</Border.Background>
|
||||
<Svg Path="{Binding Category.Icon, Converter={StaticResource SvgPathFromName}}"
|
||||
Width="17" Height="17"
|
||||
Css="{Binding Category.Color, Converter={StaticResource HexToColorConverter}, ConverterParameter=css}" />
|
||||
</Border>
|
||||
<!-- Transfer icon -->
|
||||
<Border CornerRadius="{DynamicResource RadiusIcon}"
|
||||
Width="38" Height="38"
|
||||
Background="{DynamicResource IconBgBlue}"
|
||||
IsVisible="{Binding IsTransfer}">
|
||||
<Svg Path="../Assets/Icons/arrow-right-left.svg"
|
||||
Width="17" Height="17"
|
||||
Css="{DynamicResource SvgBlue}" />
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Description + meta -->
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Category.Name}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<TextBlock Text="·"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
<TextBlock Text="{Binding Date, Converter={StaticResource DateFormatConverter}}"
|
||||
<!-- Description + meta -->
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextPrimary}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<!-- Normal category name -->
|
||||
<TextBlock Text="{Binding Category.Name}"
|
||||
FontSize="11"
|
||||
IsVisible="{Binding !IsTransfer}"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
<!-- Transfer label -->
|
||||
<TextBlock Text="Transfer"
|
||||
FontSize="11"
|
||||
IsVisible="{Binding IsTransfer}"
|
||||
Foreground="{DynamicResource AccentBlue}" />
|
||||
<TextBlock Text="·"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextDisabled}" />
|
||||
<TextBlock Text="{Binding Date, Converter={StaticResource DateFormatConverter}}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Dual-currency amount display -->
|
||||
<StackPanel Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock Text="{Binding PrimaryAmountFormatted}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
|
||||
<TextBlock Text="{Binding OriginalAmountFormatted}"
|
||||
FontSize="11"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsMultiCurrency}"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Dual-currency amount display -->
|
||||
<StackPanel Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock Text="{Binding PrimaryAmountFormatted}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
|
||||
<TextBlock Text="{Binding OriginalAmountFormatted}"
|
||||
FontSize="11"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding IsMultiCurrency}"
|
||||
Foreground="{DynamicResource TextMuted}" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
</Panel>
|
||||
</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; }
|
||||
|
||||
12
Clario/Models/CategorySpendRow.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Clario.Models;
|
||||
|
||||
public class CategorySpendRow
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public string Color { get; set; } = "#7B9CFF";
|
||||
public decimal Amount { get; set; }
|
||||
public double Percentage { get; set; }
|
||||
public string AmountFormatted { get; set; } = string.Empty;
|
||||
public string PercentageFormatted => $"{Percentage:F1}%";
|
||||
}
|
||||
@@ -35,10 +35,13 @@ public class Transaction : BaseModel
|
||||
|
||||
[Column("exchange_rate")] public decimal? ExchangeRate { get; set; }
|
||||
|
||||
[Column("transfer_pair_id")] public Guid? TransferPairId { get; set; }
|
||||
|
||||
// Set during enrichment by GeneralDataRepo.LinkTransactionAccounts
|
||||
[JsonIgnore] public string AccountCurrency { get; set; } = "";
|
||||
[JsonIgnore] public string PrimaryAmountFormatted { get; set; } = "";
|
||||
[JsonIgnore] public string OriginalAmountFormatted { get; set; } = "";
|
||||
[JsonIgnore] public string AccountDisplayText { get; set; } = "";
|
||||
|
||||
[JsonIgnore] public decimal ConvertedAmount =>
|
||||
!string.IsNullOrEmpty(AccountCurrency) && CurrencyService.LiveRates.TryGetValue(AccountCurrency, out var liveRate)
|
||||
@@ -48,5 +51,7 @@ public class Transaction : BaseModel
|
||||
[JsonIgnore] public string PrimaryAmountSignFormatted =>
|
||||
Type == "expense" ? $"-{PrimaryAmountFormatted}" : $"+{PrimaryAmountFormatted}";
|
||||
|
||||
[JsonIgnore] public bool IsTransfer => Type is "transfer_in" or "transfer_out";
|
||||
[JsonIgnore] public bool IsTransferOut => Type == "transfer_out";
|
||||
[JsonIgnore] public bool GroupHeader { get; set; } = false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
300
Clario/Services/PdfExportService.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace Clario.Services;
|
||||
|
||||
public static class PdfExportService
|
||||
{
|
||||
// Print palette (readable on white paper)
|
||||
private const string TextPrimary = "#111827";
|
||||
private const string TextSecondary = "#374151";
|
||||
private const string TextMuted = "#6B7280";
|
||||
private const string Border = "#E5E7EB";
|
||||
private const string HeaderBg = "#F3F4F6";
|
||||
private const string IncomeColor = "#15803D";
|
||||
private const string ExpenseColor = "#B91C1C";
|
||||
private const string BlueColor = "#1D4ED8";
|
||||
private const string AccentBar = "#1D4ED8"; // header rule
|
||||
|
||||
static PdfExportService()
|
||||
{
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
}
|
||||
|
||||
public static async Task<string?> ExportAsync(
|
||||
GeneralDataRepo data,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
string periodLabel,
|
||||
List<CategorySpendRow> topCategories)
|
||||
{
|
||||
var culture = new CultureInfo("en-US");
|
||||
var sym = CurrencyService.GetSymbol(data.PrimaryAccount?.Currency ?? data.Profile?.Currency ?? "USD");
|
||||
var userName = data.Profile?.DisplayName ?? "User";
|
||||
|
||||
var periodTxs = data.Transactions
|
||||
.Where(t => !t.IsTransfer && t.Date.Date >= start.Date && t.Date.Date <= end.Date)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ToList();
|
||||
|
||||
var totalIncome = periodTxs.Where(t => t.Type == "income").Sum(t => t.ConvertedAmount);
|
||||
var totalExpenses = periodTxs.Where(t => t.Type == "expense").Sum(t => t.ConvertedAmount);
|
||||
var net = totalIncome - totalExpenses;
|
||||
var savingsRate = totalIncome > 0 ? net / totalIncome * 100 : 0;
|
||||
var subtitle = $"{start.ToString("MMM d, yyyy", culture)} – {end.ToString("MMM d, yyyy", culture)}";
|
||||
var generatedAt = DateTime.Now.ToString("MMM d, yyyy 'at' h:mm tt", culture);
|
||||
|
||||
// Load logo on the calling (UI) thread before entering Task.Run
|
||||
byte[] logoBytes;
|
||||
using (var assetStream = AssetLoader.Open(new Uri("avares://Clario/Assets/Logo/logo-combined-primary-transparent-384x128.png")))
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
await assetStream.CopyToAsync(ms);
|
||||
logoBytes = ms.ToArray();
|
||||
}
|
||||
|
||||
byte[] pdfBytes = [];
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
pdfBytes = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.MarginHorizontal(1.8f, Unit.Centimetre);
|
||||
page.MarginVertical(1.5f, Unit.Centimetre);
|
||||
page.DefaultTextStyle(s => s.FontSize(10).FontColor(TextPrimary).FontFamily("Arial"));
|
||||
|
||||
// Header
|
||||
page.Header().Column(col =>
|
||||
{
|
||||
col.Item().Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(left =>
|
||||
{
|
||||
left.Item().Height(32).Image(logoBytes).FitHeight();
|
||||
left.Item().PaddingTop(4).Text($"Financial Report — {periodLabel}")
|
||||
.FontSize(12).SemiBold().FontColor(TextSecondary);
|
||||
left.Item().Text(subtitle).FontSize(9).FontColor(TextMuted);
|
||||
});
|
||||
row.ConstantItem(140).AlignRight().Column(right =>
|
||||
{
|
||||
right.Item().AlignRight().Text(userName)
|
||||
.FontSize(10).SemiBold().FontColor(TextPrimary);
|
||||
right.Item().AlignRight().Text($"Generated {generatedAt}")
|
||||
.FontSize(8).FontColor(TextMuted);
|
||||
});
|
||||
});
|
||||
col.Item().PaddingTop(10).LineHorizontal(2).LineColor(AccentBar);
|
||||
});
|
||||
|
||||
// Content
|
||||
page.Content().PaddingTop(18).Column(col =>
|
||||
{
|
||||
// KPI cards
|
||||
col.Item().Text("Summary").FontSize(11).SemiBold().FontColor(TextPrimary);
|
||||
col.Item().PaddingTop(6).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(c =>
|
||||
{
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn();
|
||||
c.RelativeColumn();
|
||||
});
|
||||
|
||||
void KpiCell(string label, string value, string valueColor)
|
||||
{
|
||||
table.Cell()
|
||||
.Border(1).BorderColor(Border)
|
||||
.Background(HeaderBg)
|
||||
.Padding(12)
|
||||
.Column(c =>
|
||||
{
|
||||
c.Item().Text(label).FontSize(8).FontColor(TextMuted);
|
||||
c.Item().PaddingTop(4).Text(value)
|
||||
.FontSize(14).Bold().FontColor(valueColor);
|
||||
});
|
||||
}
|
||||
|
||||
KpiCell("TOTAL INCOME", $"{sym}{totalIncome:N2}", IncomeColor);
|
||||
KpiCell("TOTAL EXPENSES", $"{sym}{totalExpenses:N2}", ExpenseColor);
|
||||
KpiCell("NET SAVINGS", $"{(net >= 0 ? "+" : "")}{sym}{net:N2}",
|
||||
net >= 0 ? IncomeColor : ExpenseColor);
|
||||
KpiCell("SAVINGS RATE", totalIncome > 0 ? $"{savingsRate:F1}%" : "—", BlueColor);
|
||||
});
|
||||
|
||||
col.Item().Height(20);
|
||||
|
||||
// Top categories
|
||||
if (topCategories.Count > 0)
|
||||
{
|
||||
col.Item().Text("Top Spending Categories")
|
||||
.FontSize(11).SemiBold().FontColor(TextPrimary);
|
||||
col.Item().PaddingTop(6).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(c =>
|
||||
{
|
||||
c.RelativeColumn(4);
|
||||
c.RelativeColumn(2);
|
||||
c.RelativeColumn(1);
|
||||
});
|
||||
|
||||
// Header row — CATEGORY stays left, rest centered
|
||||
void TH(string text, bool leftAlign = false)
|
||||
{
|
||||
var cell = table.Cell().Background(HeaderBg)
|
||||
.PaddingHorizontal(8).PaddingVertical(7)
|
||||
.BorderBottom(1).BorderColor(Border);
|
||||
if (leftAlign)
|
||||
cell.Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
|
||||
else
|
||||
cell.AlignCenter().Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
|
||||
}
|
||||
|
||||
TH("CATEGORY", leftAlign: true); TH("AMOUNT"); TH("SHARE");
|
||||
|
||||
foreach (var cat in topCategories)
|
||||
{
|
||||
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
|
||||
.BorderBottom(1).BorderColor(Border)
|
||||
.Text(cat.Name).FontSize(10).FontColor(TextPrimary);
|
||||
|
||||
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
|
||||
.BorderBottom(1).BorderColor(Border)
|
||||
.Text(cat.AmountFormatted).FontSize(10).FontColor(TextSecondary);
|
||||
|
||||
table.Cell().PaddingHorizontal(8).PaddingVertical(7)
|
||||
.BorderBottom(1).BorderColor(Border)
|
||||
.AlignRight()
|
||||
.Text(cat.PercentageFormatted).FontSize(10).FontColor(TextMuted);
|
||||
}
|
||||
});
|
||||
|
||||
col.Item().Height(20);
|
||||
}
|
||||
|
||||
// Transactions
|
||||
col.Item().Text($"Transactions ({periodTxs.Count})")
|
||||
.FontSize(11).SemiBold().FontColor(TextPrimary);
|
||||
col.Item().PaddingTop(6).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(c =>
|
||||
{
|
||||
c.ConstantColumn(60); // date
|
||||
c.RelativeColumn(3); // description
|
||||
c.RelativeColumn(2); // category
|
||||
c.RelativeColumn(2); // account
|
||||
c.RelativeColumn(2); // amount
|
||||
});
|
||||
|
||||
// Header — DESCRIPTION stays left, all others centered
|
||||
void TH(string text, bool leftAlign = false)
|
||||
{
|
||||
var cell = table.Cell().Background(HeaderBg)
|
||||
.PaddingHorizontal(6).PaddingVertical(7)
|
||||
.BorderBottom(2).BorderColor(Border);
|
||||
if (leftAlign)
|
||||
cell.Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
|
||||
else
|
||||
cell.AlignCenter().Text(text).FontSize(8).SemiBold().FontColor(TextMuted);
|
||||
}
|
||||
|
||||
TH("DATE"); TH("DESCRIPTION", leftAlign: true); TH("CATEGORY"); TH("ACCOUNT"); TH("AMOUNT");
|
||||
|
||||
foreach (var tx in periodTxs)
|
||||
{
|
||||
var amountStr = tx.Type == "income"
|
||||
? $"+{sym}{tx.ConvertedAmount:N2}"
|
||||
: $"-{sym}{tx.ConvertedAmount:N2}";
|
||||
var amountColor = tx.Type == "income" ? IncomeColor : ExpenseColor;
|
||||
|
||||
void TD(string text, string color = TextPrimary, bool rightAlign = false)
|
||||
{
|
||||
var cell = table.Cell()
|
||||
.PaddingHorizontal(6).PaddingVertical(6)
|
||||
.BorderBottom(1).BorderColor(Border);
|
||||
if (rightAlign)
|
||||
cell.AlignRight().Text(text).FontSize(9).FontColor(color);
|
||||
else
|
||||
cell.Text(text).FontSize(9).FontColor(color);
|
||||
}
|
||||
|
||||
TD(tx.Date.ToString("MMM d, yy", culture), TextMuted);
|
||||
TD(tx.Description);
|
||||
TD(tx.Category?.Name ?? "—", TextSecondary);
|
||||
TD(tx.AccountDisplayText, TextSecondary);
|
||||
TD(amountStr, amountColor, rightAlign: true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Footer
|
||||
page.Footer().PaddingTop(6).BorderTop(1).BorderColor(Border).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Generated by Clario — Your personal finance tracker")
|
||||
.FontSize(8).FontColor(TextMuted);
|
||||
row.ConstantItem(60).AlignRight().Text(t =>
|
||||
{
|
||||
t.AlignRight();
|
||||
t.Span("Page ").FontSize(8).FontColor(TextMuted);
|
||||
t.CurrentPageNumber().FontSize(8).FontColor(TextMuted);
|
||||
t.Span(" of ").FontSize(8).FontColor(TextMuted);
|
||||
t.TotalPages().FontSize(8).FontColor(TextMuted);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).GeneratePdf();
|
||||
});
|
||||
|
||||
return await SavePdfAsync(pdfBytes, $"Clario_Report_{DateTime.Now:yyyy-MM-dd}.pdf");
|
||||
}
|
||||
|
||||
private static async Task<string?> SavePdfAsync(byte[] pdfBytes, string suggestedName)
|
||||
{
|
||||
var topLevel = GetTopLevel();
|
||||
if (topLevel is null)
|
||||
{
|
||||
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), suggestedName);
|
||||
await File.WriteAllBytesAsync(path, pdfBytes);
|
||||
return path;
|
||||
}
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Save PDF Report",
|
||||
SuggestedFileName = suggestedName,
|
||||
FileTypeChoices = [new FilePickerFileType("PDF Document") { Patterns = ["*.pdf"] }]
|
||||
});
|
||||
|
||||
if (file is null) return null;
|
||||
|
||||
await using var stream = await file.OpenWriteAsync();
|
||||
await stream.WriteAsync(pdfBytes);
|
||||
return file.Path.LocalPath;
|
||||
}
|
||||
|
||||
private static TopLevel? GetTopLevel()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return TopLevel.GetTopLevel(desktop.MainWindow);
|
||||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime single)
|
||||
return TopLevel.GetTopLevel(single.MainView as Visual);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using Clario.Services;
|
||||
@@ -21,37 +22,61 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
[ObservableProperty] private Account? _selectedAccount;
|
||||
[ObservableProperty] private bool _isAccountDeletionConfirmationVisible;
|
||||
public bool CanDeleteAccount => VisibleAccounts.Count > 1;
|
||||
public bool CanDeleteAccount => VisibleAccounts.Count(x => !x.GroupHeader) > 1;
|
||||
public int ActiveAccountCount => VisibleAccounts.Count(x => !x.GroupHeader);
|
||||
|
||||
[ObservableProperty] private bool _isDeleteDialogVisible;
|
||||
[ObservableProperty] private DeleteAccountDialogViewModel _deleteDialog = new();
|
||||
|
||||
[ObservableProperty] private bool _isArchiveDialogVisible;
|
||||
[ObservableProperty] private Account? _accountToArchive;
|
||||
|
||||
[ObservableProperty] private bool _isArchivedListVisible;
|
||||
[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(); };
|
||||
Track(AppData.Accounts, (_, _) => Initialize());
|
||||
Track(AppData.Transactions, (_, _) => Initialize());
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
var prevSelectedId = SelectedAccount?.Id;
|
||||
FetchAndProcessAccountInfo();
|
||||
GroupAccounts();
|
||||
SelectedAccount = VisibleAccounts.FirstOrDefault(x => !x.GroupHeader);
|
||||
ArchivedAccounts = AppData.Accounts.Where(a => a.IsArchived).ToList();
|
||||
OnPropertyChanged(nameof(HasArchivedAccounts));
|
||||
OnPropertyChanged(nameof(ActiveAccountCount));
|
||||
OnPropertyChanged(nameof(CanDeleteAccount));
|
||||
// Set to null first so PropertyChanged fires even when re-selecting the same account,
|
||||
// ensuring the detail panel re-reads all computed properties (balance, income, etc.)
|
||||
SelectedAccount = null;
|
||||
SelectedAccount = (prevSelectedId.HasValue
|
||||
? VisibleAccounts.FirstOrDefault(a => a.Id == prevSelectedId && !a.GroupHeader)
|
||||
: null) ?? VisibleAccounts.FirstOrDefault(x => !x.GroupHeader);
|
||||
}
|
||||
|
||||
private void FetchAndProcessAccountInfo()
|
||||
{
|
||||
TotalBalance = 0;
|
||||
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
|
||||
foreach (var account in AppData.Accounts)
|
||||
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
|
||||
{
|
||||
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
account.TransactionsCount = accountTransactions.Count;
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
|
||||
account.TotalIncomeThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type == "income").Sum(t => t.Amount);
|
||||
account.TotalExpenseThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type == "expense").Sum(t => t.Amount);
|
||||
account.IncomeTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type == "income");
|
||||
account.ExpenseTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type == "expense");
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
|
||||
account.TotalIncomeThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type is "income" or "transfer_in").Sum(t => t.Amount);
|
||||
account.TotalExpenseThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type is "expense" or "transfer_out").Sum(t => t.Amount);
|
||||
account.IncomeTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type is "income" or "transfer_in");
|
||||
account.ExpenseTransactionsThisMonth = accountTransactions.Count(t => t.Date.Month == DateTime.Now.Month && t.Type is "expense" or "transfer_out");
|
||||
account.RecentTransactions = accountTransactions.OrderByDescending(t => t.Date).Take(3).ToList();
|
||||
var lastMonthBalance = accountTransactions.Where(t => t.Date.Month == DateTime.Now.AddMonths(-1).Month && t.Type == "income")
|
||||
.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
|
||||
@@ -59,7 +84,7 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
|
||||
TotalBalance += account.CurrentBalance;
|
||||
else
|
||||
TotalBalance += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
|
||||
TotalBalance += accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +116,7 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
foreach (var type in accountTypes)
|
||||
{
|
||||
var accountsOfType = AppData.Accounts
|
||||
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase) && !a.IsArchived)
|
||||
.OrderByDescending(a => a.IsPrimary)
|
||||
.ThenBy(a => a.CreatedAt)
|
||||
.ToList();
|
||||
@@ -109,6 +134,53 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanDeleteAccount));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RequestArchiveAccount(Account account)
|
||||
{
|
||||
AccountToArchive = account;
|
||||
IsArchiveDialogVisible = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelArchive()
|
||||
{
|
||||
IsArchiveDialogVisible = false;
|
||||
AccountToArchive = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmArchive()
|
||||
{
|
||||
if (AccountToArchive is null) return;
|
||||
AccountToArchive.IsArchived = true;
|
||||
await AppData.UpdateAccount(AccountToArchive);
|
||||
IsArchiveDialogVisible = false;
|
||||
AccountToArchive = null;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowArchivedList()
|
||||
{
|
||||
IsArchivedListVisible = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseArchivedList()
|
||||
{
|
||||
IsArchivedListVisible = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UnarchiveAccount(Account account)
|
||||
{
|
||||
account.IsArchived = false;
|
||||
await AppData.UpdateAccount(account);
|
||||
Initialize();
|
||||
if (!HasArchivedAccounts)
|
||||
IsArchivedListVisible = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RequestDeleteAccount(Account account)
|
||||
{
|
||||
@@ -117,6 +189,7 @@ public partial class AccountsViewModel : ViewModelBase
|
||||
{
|
||||
IsDeleteDialogVisible = false;
|
||||
Initialize();
|
||||
ShouldCloseSheet = true;
|
||||
};
|
||||
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
|
||||
IsDeleteDialogVisible = true;
|
||||
|
||||
439
Clario/ViewModels/AnalyticsViewModel.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.SkiaSharpView;
|
||||
using LiveChartsCore.SkiaSharpView.Painting;
|
||||
using SkiaSharp;
|
||||
using SKColor = SkiaSharp.SKColor;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class AnalyticsViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
public GeneralDataRepo AppData => DataRepo.General;
|
||||
|
||||
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"
|
||||
};
|
||||
|
||||
[ObservableProperty] private string _selectedPeriod = "Last 6 Months";
|
||||
|
||||
partial void OnSelectedPeriodChanged(string value) => Initialize();
|
||||
|
||||
// KPI cards
|
||||
[ObservableProperty] private string _totalIncomeFormatted = "—";
|
||||
[ObservableProperty] private string _totalExpensesFormatted = "—";
|
||||
[ObservableProperty] private string _netSavingsFormatted = "—";
|
||||
[ObservableProperty] private string _savingsRateFormatted = "—";
|
||||
[ObservableProperty] private bool _netSavingsPositive = true;
|
||||
|
||||
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
|
||||
// Cash Flow chart
|
||||
[ObservableProperty] private ISeries[] _cashFlowSeries = [];
|
||||
[ObservableProperty] private Axis[] _cashFlowXAxes = [];
|
||||
[ObservableProperty] private Axis[] _cashFlowYAxes = [];
|
||||
|
||||
// Net Worth chart
|
||||
[ObservableProperty] private ISeries[] _netWorthSeries = [];
|
||||
[ObservableProperty] private Axis[] _netWorthXAxes = [];
|
||||
[ObservableProperty] private Axis[] _netWorthYAxes = [];
|
||||
|
||||
// Day-of-week chart
|
||||
[ObservableProperty] private ISeries[] _dayOfWeekSeries = [];
|
||||
[ObservableProperty] private Axis[] _dayOfWeekXAxes = [];
|
||||
|
||||
// Top categories
|
||||
[ObservableProperty] private ObservableCollection<CategorySpendRow> _topCategories = new();
|
||||
[ObservableProperty] private bool _hasTopCategories;
|
||||
|
||||
// Income sources donut
|
||||
[ObservableProperty] private ISeries[] _incomeSourcesSeries = [];
|
||||
[ObservableProperty] private bool _hasIncomeSources;
|
||||
|
||||
// State
|
||||
[ObservableProperty] private bool _isExporting;
|
||||
[ObservableProperty] private string? _exportStatusMessage;
|
||||
|
||||
//
|
||||
|
||||
public AnalyticsViewModel()
|
||||
{
|
||||
Track(AppData.Transactions, (_, _) => Initialize());
|
||||
Track(AppData.Accounts, (_, _) => Initialize());
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (start, end) = GetDateRange();
|
||||
var periodTxs = AppData.Transactions
|
||||
.Where(t => !t.IsTransfer && t.Date.Date >= start.Date && t.Date.Date <= end.Date)
|
||||
.ToList();
|
||||
|
||||
var expenses = periodTxs.Where(t => t.Type == "expense").ToList();
|
||||
var income = periodTxs.Where(t => t.Type == "income").ToList();
|
||||
|
||||
ComputeKpis(income, expenses);
|
||||
BuildCashFlowChart(start, end);
|
||||
BuildNetWorthChart(start, end);
|
||||
BuildDayOfWeekChart(expenses, start, end);
|
||||
BuildTopCategories(expenses);
|
||||
BuildIncomeSourcesChart(income);
|
||||
OnPropertyChanged(nameof(PrimarySymbol));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Date range
|
||||
|
||||
private (DateTime start, DateTime end) GetDateRange()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
return SelectedPeriod switch
|
||||
{
|
||||
"Last 30 Days" => (now.AddDays(-30), now),
|
||||
"Last 3 Months" => (now.AddMonths(-3), now),
|
||||
"Last 6 Months" => (now.AddMonths(-6), now),
|
||||
"Last 12 Months" => (now.AddMonths(-12), now),
|
||||
"This Year" => (new DateTime(now.Year, 1, 1), now),
|
||||
_ => (now.AddMonths(-6), now)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<(DateTime monthStart, DateTime monthEnd, string label)> GetMonthBuckets(DateTime start, DateTime end)
|
||||
{
|
||||
var buckets = new List<(DateTime, DateTime, string)>();
|
||||
var current = new DateTime(start.Year, start.Month, 1);
|
||||
var endMonth = new DateTime(end.Year, end.Month, 1);
|
||||
var culture = new CultureInfo("en-US");
|
||||
while (current <= endMonth)
|
||||
{
|
||||
var next = current.AddMonths(1);
|
||||
buckets.Add((current, next.AddSeconds(-1), current.ToString("MMM ''yy", culture)));
|
||||
current = next;
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
// Section 1: KPIs
|
||||
|
||||
private void ComputeKpis(List<Transaction> income, List<Transaction> expenses)
|
||||
{
|
||||
var sym = PrimarySymbol;
|
||||
var totalIncome = income.Sum(t => t.ConvertedAmount);
|
||||
var totalExpenses = expenses.Sum(t => t.ConvertedAmount);
|
||||
var net = totalIncome - totalExpenses;
|
||||
|
||||
TotalIncomeFormatted = $"{sym}{totalIncome:N2}";
|
||||
TotalExpensesFormatted = $"{sym}{totalExpenses:N2}";
|
||||
NetSavingsPositive = net >= 0;
|
||||
NetSavingsFormatted = $"{(net >= 0 ? "+" : "")}{sym}{net:N2}";
|
||||
SavingsRateFormatted = totalIncome > 0
|
||||
? $"{Math.Max(0, (net / totalIncome) * 100):F1}%"
|
||||
: "—";
|
||||
}
|
||||
|
||||
// Section 2: Cash Flow
|
||||
|
||||
private void BuildCashFlowChart(DateTime start, DateTime end)
|
||||
{
|
||||
var buckets = GetMonthBuckets(start, end);
|
||||
var incomeVals = new double[buckets.Count];
|
||||
var expenseVals = new double[buckets.Count];
|
||||
|
||||
for (var i = 0; i < buckets.Count; i++)
|
||||
{
|
||||
var (mStart, mEnd, _) = buckets[i];
|
||||
incomeVals[i] = (double)AppData.Transactions
|
||||
.Where(t => t.Type == "income" && t.Date.Date >= mStart && t.Date.Date <= mEnd)
|
||||
.Sum(t => t.ConvertedAmount);
|
||||
expenseVals[i] = (double)AppData.Transactions
|
||||
.Where(t => t.Type == "expense" && t.Date.Date >= mStart && t.Date.Date <= mEnd)
|
||||
.Sum(t => t.ConvertedAmount);
|
||||
}
|
||||
|
||||
var labels = buckets.Select(b => b.label).ToArray();
|
||||
|
||||
CashFlowSeries =
|
||||
[
|
||||
new LineSeries<double>
|
||||
{
|
||||
Name = "Income",
|
||||
Values = incomeVals,
|
||||
Stroke = new SolidColorPaint(SKColor.Parse("#2ECC8A"), 2),
|
||||
Fill = null,
|
||||
GeometryFill = new SolidColorPaint(SKColor.Parse("#2ECC8A")),
|
||||
GeometryStroke = null,
|
||||
GeometrySize = 5,
|
||||
LineSmoothness = 0.5
|
||||
},
|
||||
new LineSeries<double>
|
||||
{
|
||||
Name = "Expenses",
|
||||
Values = expenseVals,
|
||||
Stroke = new SolidColorPaint(SKColor.Parse("#FF5E5E"), 2),
|
||||
Fill = null,
|
||||
GeometryFill = new SolidColorPaint(SKColor.Parse("#FF5E5E")),
|
||||
GeometryStroke = null,
|
||||
GeometrySize = 5,
|
||||
LineSmoothness = 0.5
|
||||
}
|
||||
];
|
||||
|
||||
CashFlowXAxes =
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
Labels = labels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
}
|
||||
];
|
||||
|
||||
var sym = PrimarySymbol;
|
||||
CashFlowYAxes =
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 10,
|
||||
Labeler = v => $"{sym}{v:N0}"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Section 3: Net Worth
|
||||
|
||||
private void BuildNetWorthChart(DateTime start, DateTime end)
|
||||
{
|
||||
// Start from 12 months before start to show history, but respect the selected range
|
||||
var buckets = GetMonthBuckets(start, end);
|
||||
var netWorthVals = new double[buckets.Count];
|
||||
|
||||
for (var i = 0; i < buckets.Count; i++)
|
||||
{
|
||||
var (_, mEnd, _) = buckets[i];
|
||||
double nw = 0;
|
||||
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
|
||||
{
|
||||
var txUpTo = AppData.Transactions.Where(t => t.AccountId == account.Id && t.Date.Date <= mEnd.Date);
|
||||
nw += (double)(account.OpeningBalance +
|
||||
txUpTo.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount));
|
||||
}
|
||||
netWorthVals[i] = nw;
|
||||
}
|
||||
|
||||
var labels = buckets.Select(b => b.label).ToArray();
|
||||
|
||||
NetWorthSeries =
|
||||
[
|
||||
new LineSeries<double>
|
||||
{
|
||||
Name = "Net Worth",
|
||||
Values = netWorthVals,
|
||||
Stroke = new SolidColorPaint(SKColor.Parse("#7B9CFF"), 2),
|
||||
Fill = new SolidColorPaint(SKColor.Parse("#7B9CFF").WithAlpha(25)),
|
||||
GeometryFill = new SolidColorPaint(SKColor.Parse("#7B9CFF")),
|
||||
GeometryStroke = null,
|
||||
GeometrySize = 5,
|
||||
LineSmoothness = 0.5
|
||||
}
|
||||
];
|
||||
|
||||
NetWorthXAxes =
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
Labels = labels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
}
|
||||
];
|
||||
|
||||
var sym = PrimarySymbol;
|
||||
NetWorthYAxes =
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = new SolidColorPaint(new SKColor(30, 35, 48)),
|
||||
TicksPaint = null,
|
||||
TextSize = 10,
|
||||
Labeler = v => $"{sym}{v:N0}"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Section 4: Day of Week
|
||||
|
||||
private void BuildDayOfWeekChart(List<Transaction> expenses, DateTime start, DateTime end)
|
||||
{
|
||||
// DayOfWeek: Sunday=0, Monday=1 ... Saturday=6
|
||||
// We display Mon–Sun (index 0–6 in our array)
|
||||
var dayLabels = new[] { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
|
||||
var totals = new double[7];
|
||||
var counts = new int[7];
|
||||
|
||||
// Count occurrences of each weekday in the period
|
||||
var d = start.Date;
|
||||
while (d <= end.Date)
|
||||
{
|
||||
var idx = ((int)d.DayOfWeek + 6) % 7; // Mon=0..Sun=6
|
||||
counts[idx]++;
|
||||
d = d.AddDays(1);
|
||||
}
|
||||
|
||||
foreach (var tx in expenses)
|
||||
{
|
||||
var idx = ((int)tx.Date.DayOfWeek + 6) % 7;
|
||||
totals[idx] += (double)tx.ConvertedAmount;
|
||||
}
|
||||
|
||||
var averages = totals.Select((total, i) => counts[i] > 0 ? total / counts[i] : 0).ToArray();
|
||||
|
||||
DayOfWeekSeries =
|
||||
[
|
||||
new ColumnSeries<double>
|
||||
{
|
||||
Name = "Avg Daily Spend",
|
||||
Values = averages,
|
||||
Fill = new SolidColorPaint(SKColor.Parse("#9B7BFF")),
|
||||
MaxBarWidth = 40,
|
||||
Padding = 3
|
||||
}
|
||||
];
|
||||
|
||||
DayOfWeekXAxes =
|
||||
[
|
||||
new Axis
|
||||
{
|
||||
Labels = dayLabels,
|
||||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7A8090")) { SKTypeface = _interTypeface },
|
||||
SeparatorsPaint = null,
|
||||
TicksPaint = null,
|
||||
TextSize = 11
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Section 5: Top Categories
|
||||
|
||||
private void BuildTopCategories(List<Transaction> expenses)
|
||||
{
|
||||
var sym = PrimarySymbol;
|
||||
var totalSpend = expenses.Sum(t => t.ConvertedAmount);
|
||||
if (totalSpend == 0)
|
||||
{
|
||||
TopCategories = new ObservableCollection<CategorySpendRow>();
|
||||
HasTopCategories = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var grouped = expenses
|
||||
.Where(t => t.Category is not null)
|
||||
.GroupBy(t => t.Category!)
|
||||
.Select(g => new CategorySpendRow
|
||||
{
|
||||
Name = g.Key.Name,
|
||||
Icon = g.Key.Icon,
|
||||
Color = g.Key.Color,
|
||||
Amount = g.Sum(t => t.ConvertedAmount),
|
||||
Percentage = (double)(g.Sum(t => t.ConvertedAmount) / totalSpend * 100),
|
||||
AmountFormatted = $"{sym}{g.Sum(t => t.ConvertedAmount):N2}"
|
||||
})
|
||||
.OrderByDescending(r => r.Amount)
|
||||
.Take(8)
|
||||
.ToList();
|
||||
|
||||
TopCategories = new ObservableCollection<CategorySpendRow>(grouped);
|
||||
HasTopCategories = grouped.Count > 0;
|
||||
}
|
||||
|
||||
// Section 6: Income Sources
|
||||
|
||||
private void BuildIncomeSourcesChart(List<Transaction> income)
|
||||
{
|
||||
var grouped = income
|
||||
.Where(t => t.Category is not null)
|
||||
.GroupBy(t => t.Category!)
|
||||
.Select(g => (category: g.Key, total: g.Sum(t => t.ConvertedAmount)))
|
||||
.OrderByDescending(x => x.total)
|
||||
.ToList();
|
||||
|
||||
if (grouped.Count < 2)
|
||||
{
|
||||
IncomeSourcesSeries = [];
|
||||
HasIncomeSources = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var sym = PrimarySymbol;
|
||||
IncomeSourcesSeries = grouped.Select(x => (ISeries)new PieSeries<double>
|
||||
{
|
||||
Name = x.category.Name,
|
||||
Values = new[] { (double)x.total },
|
||||
Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)),
|
||||
InnerRadius = 20,
|
||||
ToolTipLabelFormatter = p => $"{sym}{p.Coordinate.PrimaryValue:N2}"
|
||||
}).ToArray();
|
||||
|
||||
HasIncomeSources = true;
|
||||
}
|
||||
|
||||
// PDF Export
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ExportPdf()
|
||||
{
|
||||
if (IsExporting) return;
|
||||
IsExporting = true;
|
||||
ExportStatusMessage = null;
|
||||
try
|
||||
{
|
||||
var (start, end) = GetDateRange();
|
||||
var path = await PdfExportService.ExportAsync(
|
||||
AppData,
|
||||
start,
|
||||
end,
|
||||
SelectedPeriod,
|
||||
TopCategories.ToList());
|
||||
|
||||
ExportStatusMessage = path is not null ? "PDF saved successfully." : null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ExportStatusMessage = "Export failed. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsExporting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Clario.Enums;
|
||||
using Clario.Models;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Supabase.Gotrue;
|
||||
using Supabase.Gotrue.Exceptions;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
@@ -22,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))]
|
||||
@@ -32,10 +34,17 @@ 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()
|
||||
{
|
||||
DebugLogger.Log("auth vm loaded");
|
||||
@@ -64,11 +73,37 @@ public partial class AuthViewModel : ViewModelBase
|
||||
private void SetOperation(string operation)
|
||||
{
|
||||
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))]
|
||||
private async Task ConfirmLogin()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
try
|
||||
{
|
||||
await SupabaseService.Client.Auth.SignIn(_email, _password);
|
||||
@@ -84,15 +119,22 @@ public partial class AuthViewModel : ViewModelBase
|
||||
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
}
|
||||
}
|
||||
catch (GotrueException e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = GetLoginErrorMessage(e.Reason);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = GetErrorMessage(AuthError.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(canCreateAccount))]
|
||||
private async Task ConfirmCreateAccount()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
try
|
||||
{
|
||||
var session = await SupabaseService.Client.Auth.SignUp(
|
||||
@@ -120,21 +162,65 @@ public partial class AuthViewModel : ViewModelBase
|
||||
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
||||
}
|
||||
}
|
||||
catch (GotrueException e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = GetSignupErrorMessage(e.Reason);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugLogger.Log(e);
|
||||
ErrorMessage = GetErrorMessage(AuthError.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLoginErrorMessage(FailureHint.Reason reason) => reason switch
|
||||
{
|
||||
FailureHint.Reason.UserBadLogin => GetErrorMessage(AuthError.InvalidCredentials),
|
||||
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.InvalidCredentials),
|
||||
FailureHint.Reason.UserEmailNotConfirmed => GetErrorMessage(AuthError.EmailNotConfirmed),
|
||||
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
|
||||
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
|
||||
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
|
||||
_ => GetErrorMessage(AuthError.Unknown),
|
||||
};
|
||||
|
||||
private static string GetSignupErrorMessage(FailureHint.Reason reason) => reason switch
|
||||
{
|
||||
FailureHint.Reason.UserAlreadyRegistered => GetErrorMessage(AuthError.EmailAlreadyExists),
|
||||
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.WeakPassword),
|
||||
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
|
||||
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
|
||||
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
|
||||
_ => GetErrorMessage(AuthError.Unknown),
|
||||
};
|
||||
|
||||
private static string GetErrorMessage(AuthError error) => error switch
|
||||
{
|
||||
AuthError.InvalidCredentials => "Invalid email or password.",
|
||||
AuthError.EmailAlreadyExists => "An account with this email already exists.",
|
||||
AuthError.EmailNotConfirmed => "Please confirm your email before signing in.",
|
||||
AuthError.WeakPassword => "Password must be at least 6 characters.",
|
||||
AuthError.InvalidEmail => "Please enter a valid email address.",
|
||||
AuthError.SignupDisabled => "Sign-ups are currently disabled.",
|
||||
AuthError.RateLimited => "Too many attempts. Please wait and try again.",
|
||||
AuthError.SessionExpired => "Your session has expired. Please sign in again.",
|
||||
_ => "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -28,8 +28,8 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))]
|
||||
private DateTime _currentPeriod = DateTime.Now.Date;
|
||||
|
||||
public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month;
|
||||
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && CurrentPeriod.Month > AppData.Transactions.Min(x => x.Date.Month);
|
||||
public bool CanGoToNextPeriod => CurrentPeriod.Year < DateTime.Now.Year || (CurrentPeriod.Year == DateTime.Now.Year && CurrentPeriod.Month < DateTime.Now.Month);
|
||||
public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && new DateTime(CurrentPeriod.Year, CurrentPeriod.Month, 1) > new DateTime(AppData.Transactions.Min(x => x.Date).Year, AppData.Transactions.Min(x => x.Date).Month, 1);
|
||||
public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy");
|
||||
|
||||
[ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = [];
|
||||
@@ -40,7 +40,7 @@ public partial class BudgetViewModel : ViewModelBase
|
||||
public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget.";
|
||||
|
||||
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
|
||||
private string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
|
||||
public string TotalLeftFormatted => $"{PrimarySymbol}{TotalLeft:N0} left";
|
||||
|
||||
public bool HasSavingsGoal => AppData.Profile?.SavingsGoal is > 0;
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
192
Clario/ViewModels/CategoryFormViewModel.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Clario.Data;
|
||||
using Clario.Models;
|
||||
using Clario.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Clario.ViewModels;
|
||||
|
||||
public partial class CategoryFormViewModel : ViewModelBase
|
||||
{
|
||||
public required ViewModelBase parentViewModel;
|
||||
|
||||
// Mode
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel), nameof(CanDelete))]
|
||||
private bool _isEditMode = false;
|
||||
|
||||
public string FormTitle => IsEditMode ? "Edit Category" : "New Category";
|
||||
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
|
||||
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Category";
|
||||
|
||||
// Fields
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
|
||||
private string _name = "";
|
||||
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsExpense), nameof(IsIncome))]
|
||||
private string _type = "expense";
|
||||
|
||||
[ObservableProperty] private string _selectedIcon = "utensils";
|
||||
|
||||
[ObservableProperty] private string _selectedColor = "#7B9CFF";
|
||||
|
||||
// Icon options
|
||||
public List<string> CategoryIcons { get; } = new()
|
||||
{
|
||||
// Food & Dining
|
||||
"utensils", "hamburger", "coffee", "pizza", "wine",
|
||||
// Shopping
|
||||
"shopping-cart", "shopping-bag", "package", "gift", "shirt",
|
||||
// Transport
|
||||
"car", "bus", "train-front", "bike", "plane",
|
||||
// Home & Utilities
|
||||
"house", "zap", "wifi", "plug-2", "wrench",
|
||||
// Health & Fitness
|
||||
"heart-pulse", "pill", "dumbbell", "scissors", "stethoscope",
|
||||
// Entertainment
|
||||
"gamepad-2", "film", "music", "tv", "headphones",
|
||||
// Finance
|
||||
"banknote", "credit-card", "piggy-bank", "wallet", "hand-coins",
|
||||
"trending-up", "trending-down", "landmark", "circle-dollar-sign", "gem",
|
||||
// Work & Education
|
||||
"briefcase", "graduation-cap", "book-open", "target", "mail",
|
||||
// Personal & Lifestyle
|
||||
"heart", "moon", "sun", "leaf", "camera",
|
||||
// Bills & Subscriptions
|
||||
"receipt", "receipt-text", "smartphone", "volume-2", "refresh-cw",
|
||||
};
|
||||
|
||||
// Validation
|
||||
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
||||
private string? _errorMessage;
|
||||
|
||||
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
||||
public bool IsExpense => Type == "expense";
|
||||
public bool IsIncome => Type == "income";
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
|
||||
public bool CanDelete => IsEditMode && DataRepo.General.Categories.Count > 4;
|
||||
|
||||
// Delete confirm sub-modal
|
||||
[ObservableProperty] private bool _showDeleteConfirm = false;
|
||||
|
||||
// Callbacks
|
||||
public Action? OnSaved;
|
||||
public Action? OnCancelled;
|
||||
public Action? OnDeleted;
|
||||
|
||||
// Edit mode: original category
|
||||
private Guid? _editingId;
|
||||
|
||||
// Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void SetType(string type) => Type = type;
|
||||
|
||||
[RelayCommand]
|
||||
private void SetIcon(string icon) => SelectedIcon = icon;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Save()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
ErrorMessage = "Name is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (IsEditMode && _editingId.HasValue)
|
||||
{
|
||||
var updated = new Category
|
||||
{
|
||||
Id = _editingId.Value,
|
||||
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
|
||||
Name = Name.Trim(),
|
||||
Type = Type,
|
||||
Icon = SelectedIcon,
|
||||
Color = SelectedColor,
|
||||
};
|
||||
await DataRepo.General.UpdateCategory(updated);
|
||||
}
|
||||
else
|
||||
{
|
||||
var category = new Category
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
|
||||
Name = Name.Trim(),
|
||||
Type = Type,
|
||||
Icon = SelectedIcon,
|
||||
Color = SelectedColor,
|
||||
};
|
||||
await DataRepo.General.InsertCategory(category);
|
||||
}
|
||||
|
||||
OnSaved?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Something went wrong. Please try again.";
|
||||
DebugLogger.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => OnCancelled?.Invoke();
|
||||
|
||||
[RelayCommand]
|
||||
private void RequestDelete() => ShowDeleteConfirm = true;
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelDelete() => ShowDeleteConfirm = false;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmDelete()
|
||||
{
|
||||
if (!IsEditMode || !_editingId.HasValue) return;
|
||||
|
||||
try
|
||||
{
|
||||
await DataRepo.General.DeleteCategory(_editingId.Value);
|
||||
OnDeleted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to delete category.";
|
||||
DebugLogger.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Public setup methods
|
||||
|
||||
public void SetupForAdd()
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = false;
|
||||
_editingId = null;
|
||||
Name = "";
|
||||
Type = "expense";
|
||||
SelectedIcon = "utensils";
|
||||
SelectedColor = "#7B9CFF";
|
||||
ErrorMessage = null;
|
||||
}
|
||||
|
||||
public void SetupForEdit(Category category)
|
||||
{
|
||||
ShowDeleteConfirm = false;
|
||||
IsEditMode = true;
|
||||
_editingId = category.Id;
|
||||
Name = category.Name;
|
||||
Type = category.Type;
|
||||
SelectedIcon = category.Icon;
|
||||
SelectedColor = category.Color;
|
||||
ErrorMessage = null;
|
||||
OnPropertyChanged(nameof(CanDelete));
|
||||
}
|
||||
}
|
||||
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,47 +162,50 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CreateTransaction()
|
||||
{
|
||||
((MainViewModel)parentViewModel).OpenAddTransaction();
|
||||
if (parentViewModel is MainViewModel main) main.OpenAddTransaction();
|
||||
}
|
||||
|
||||
private void UpdateSpendingByCategoryChart(ChartTimePeriod period = ChartTimePeriod.ThisMonth)
|
||||
[RelayCommand]
|
||||
private void NavigateToSettings()
|
||||
{
|
||||
if (parentViewModel is MainViewModel main) main.GoToSettingsCommand.Execute(null);
|
||||
}
|
||||
|
||||
[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();
|
||||
@@ -241,7 +231,7 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
|
||||
private void UpdateRecentTransactions()
|
||||
{
|
||||
RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.OrderByDescending(x => x.Date).Take(5));
|
||||
RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.Where(x => !x.IsTransfer).OrderByDescending(x => x.Date).Take(5));
|
||||
OnPropertyChanged(nameof(HasTransactionData));
|
||||
}
|
||||
|
||||
@@ -249,25 +239,17 @@ public partial class DashboardViewModel : ViewModelBase
|
||||
{
|
||||
TotalNetworth = 0;
|
||||
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
|
||||
foreach (var account in AppData.Accounts)
|
||||
foreach (var account in AppData.Accounts.Where(a => !a.IsArchived))
|
||||
{
|
||||
var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
|
||||
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.Amount : -t.Amount);
|
||||
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
|
||||
TotalNetworth += account.CurrentBalance;
|
||||
else
|
||||
TotalNetworth += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
|
||||
TotalNetworth += accountTransactions.Sum(t => t.Type is "income" or "transfer_in" ? t.ConvertedAmount : -t.ConvertedAmount);
|
||||
}
|
||||
|
||||
AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.OrderBy(x => x.CreatedAt));
|
||||
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,6 +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;
|
||||
@@ -28,18 +31,24 @@ public partial class MainViewModel : ViewModelBase
|
||||
[ObservableProperty] private TransactionFormViewModel _transactionFormViewModel = null!;
|
||||
[ObservableProperty] private AccountFormViewModel _accountFormViewModel = null!;
|
||||
[ObservableProperty] private BudgetFormViewModel _budgetFormViewModel = null!;
|
||||
[ObservableProperty] private CategoryFormViewModel _categoryFormViewModel = null!;
|
||||
[ObservableProperty] private SettingsViewModel _settingsViewModel = null!;
|
||||
[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;
|
||||
[ObservableProperty] private bool _isCategoryFormVisible;
|
||||
[ObservableProperty] private bool _isSavingsGoalDialogVisible;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(isOnDashboard), nameof(isOnTransactions), nameof(isOnAccounts), nameof(isOnBudget), 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;
|
||||
@@ -47,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();
|
||||
}
|
||||
|
||||
@@ -70,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");
|
||||
});
|
||||
|
||||
@@ -86,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
|
||||
@@ -103,6 +113,21 @@ 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
|
||||
};
|
||||
DebugLogger.Log("initialized AnalyticsViewModel");
|
||||
SettingsViewModel = new SettingsViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
@@ -112,6 +137,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
TransactionFormViewModel.OnOpenCategoryForm = OpenAddCategoryFromTransactionForm;
|
||||
TransactionFormViewModel.OnOpenEditCategoryForm = OpenEditCategoryFromTransactionForm;
|
||||
DebugLogger.Log("initialized TransactionFormViewModel");
|
||||
AccountFormViewModel = new AccountFormViewModel()
|
||||
{
|
||||
@@ -123,12 +150,18 @@ public partial class MainViewModel : ViewModelBase
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized BudgetFormViewModel");
|
||||
CategoryFormViewModel = new CategoryFormViewModel()
|
||||
{
|
||||
parentViewModel = this
|
||||
};
|
||||
DebugLogger.Log("initialized CategoryFormViewModel");
|
||||
SetSavingsGoalDialogViewModel = new SetSavingsGoalDialogViewModel();
|
||||
DebugLogger.Log("initialized SetSavingsGoalDialogViewModel");
|
||||
|
||||
IsDarkTheme = ThemeService.IsDarkTheme;
|
||||
|
||||
ThemeService.SwitchToTheme(AppData.Profile?.Theme ?? "system");
|
||||
AppData.StartRealtimeSync();
|
||||
CurrentView = _dashboardViewModel;
|
||||
IsDimmed = false;
|
||||
}
|
||||
@@ -138,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()
|
||||
{
|
||||
@@ -268,6 +319,78 @@ public partial class MainViewModel : ViewModelBase
|
||||
IsBudgetFormVisible = false;
|
||||
}
|
||||
|
||||
private void OpenEditCategoryFromTransactionForm(Category category)
|
||||
{
|
||||
CategoryFormViewModel.SetupForEdit(category);
|
||||
CategoryFormViewModel.OnSaved = () =>
|
||||
{
|
||||
TransactionFormViewModel.Categories = AppData.Categories;
|
||||
// Keep the selected category in sync after edit
|
||||
var updated = AppData.Categories.FirstOrDefault(c => c.Id == category.Id);
|
||||
if (updated is not null) TransactionFormViewModel.SelectedCategory = updated;
|
||||
CloseCategoryForm();
|
||||
};
|
||||
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnDeleted = () =>
|
||||
{
|
||||
TransactionFormViewModel.Categories = AppData.Categories;
|
||||
TransactionFormViewModel.SelectedCategory = AppData.Categories.FirstOrDefault(c => c.Type == TransactionFormViewModel.Type);
|
||||
CloseCategoryForm();
|
||||
};
|
||||
IsCategoryFormVisible = true;
|
||||
}
|
||||
|
||||
// Called by the plus button inside TransactionFormView
|
||||
private void OpenAddCategoryFromTransactionForm()
|
||||
{
|
||||
CategoryFormViewModel.SetupForAdd();
|
||||
CategoryFormViewModel.OnSaved = () =>
|
||||
{
|
||||
// Refresh the category list in the transaction form after adding
|
||||
TransactionFormViewModel.Categories = AppData.Categories;
|
||||
CloseCategoryForm();
|
||||
};
|
||||
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnDeleted = () =>
|
||||
{
|
||||
TransactionFormViewModel.Categories = AppData.Categories;
|
||||
CloseCategoryForm();
|
||||
};
|
||||
IsCategoryFormVisible = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenAddCategory()
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
CategoryFormViewModel.SetupForAdd();
|
||||
CategoryFormViewModel.OnSaved = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnDeleted = CloseCategoryForm;
|
||||
IsCategoryFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenEditCategory(Category category)
|
||||
{
|
||||
if (IsDimmed) return;
|
||||
CategoryFormViewModel.SetupForEdit(category);
|
||||
CategoryFormViewModel.OnSaved = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnCancelled = CloseCategoryForm;
|
||||
CategoryFormViewModel.OnDeleted = CloseCategoryForm;
|
||||
IsCategoryFormVisible = true;
|
||||
IsDimmed = true;
|
||||
}
|
||||
|
||||
private void CloseCategoryForm()
|
||||
{
|
||||
IsCategoryFormVisible = false;
|
||||
// Only clear the dim if no other modal is open
|
||||
if (!IsTransactionFormVisible)
|
||||
IsDimmed = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenEditSavingsGoal()
|
||||
{
|
||||
@@ -316,6 +439,24 @@ public partial class MainViewModel : ViewModelBase
|
||||
CurrentView = _budgetViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToCategories()
|
||||
{
|
||||
CurrentView = _categoriesViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToMore()
|
||||
{
|
||||
CurrentView = _moreViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToAnalytics()
|
||||
{
|
||||
CurrentView = _analyticsViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoToSettings()
|
||||
{
|
||||
@@ -339,9 +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;
|
||||
}
|
||||