18 Commits

Author SHA1 Message Date
d8dea1913a Added multi-currency support, account/budget management, and settings
All checks were successful
Build Linux / build (push) Successful in 1m8s
- Primary account determines app-wide reference currency; all totals, charts, and summaries convert to it automatically using live rates

- Transactions show both converted and original amounts for cross-currency accounts; IsMultiCurrency recalculates on primary currency change

- Exchange rates fetched live on account save and broadcast via RatesRefreshed so all views update without a restart

- Account create/edit/delete with currency, icon, color, and primary toggle

- Budget create/edit/delete; savings goal dialog

- Settings view: display name, avatar upload, theme, language

- Removed currency selector from Settings (follows primary account)

- Fixed account list sort: primary first, then oldest CreatedAt, per group

- Fixed total balance overlap in dashboard accounts card
2026-04-03 02:39:51 +03:00
1f99e49dec fixed logo in readme 2026-04-01 22:15:22 +03:00
8bac9fbc58 fixed logo in readme 2026-04-01 22:14:41 +03:00
e0aad6277d added readme 2026-04-01 22:13:23 +03:00
99ce4b8e55 a
All checks were successful
Build Linux / build (push) Successful in 1m10s
2026-04-01 21:46:25 +03:00
bdf52e82af Added Budgets Create/Update/Delete, Account Create/Update/Delete, and fully added settings tab, and refactored a lot of the data logic 2026-04-01 21:34:36 +03:00
a8244ec0de changing action content 2026-03-28 18:57:20 +03:00
2affd56e38 explicitly set skiasharp version correctly this time 2026-03-28 18:28:41 +03:00
6714cccf1d explicitly set skiasharp version 2026-03-28 18:21:54 +03:00
06575fb224 fixed upload section i think 2026-03-28 18:12:48 +03:00
68c19a9adf seperated github and gitea workflows 2026-03-28 18:04:11 +03:00
ebf7aec77c fixed action 2026-03-28 18:01:24 +03:00
5c6a5fb41d fixed action 2026-03-28 18:00:51 +03:00
3754c67449 fixed action 2026-03-28 17:59:27 +03:00
582d6b5663 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.github/workflows/build-linux.yml
2026-03-28 17:56:10 +03:00
fc5c8d7b51 fixed path 2026-03-28 17:53:12 +03:00
Nouredeen Ghazal
0f8d0867ad Add packaging step for Linux build as tar.gz 2026-03-28 15:03:39 +03:00
Nouredeen Ghazal
0c782fd9b4 Update build workflow to publish Clario.Desktop
fixed path
2026-03-28 15:01:21 +03:00
132 changed files with 7528 additions and 1243 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")",
"Bash(dotnet build:*)"
]
},
"spinnerTipsEnabled": true
}

View File

@@ -0,0 +1,258 @@
---
name: avalonia
description: >
Use when working on any Avalonia UI code — AXAML, control styling, bindings,
control templates, animations, custom controls, platform differences, or
LiveCharts2/Svg.Skia integration. Triggers on questions about Avalonia
controls, properties, ControlThemes, styles, pseudo-classes, DataTemplates,
ViewLocator, or any "how do I do X in Avalonia" question.
---
# Avalonia UI Skill
You are working in an Avalonia UI project. This skill gives you accurate,
verified knowledge about Avalonia and prevents hallucinating WPF-style patterns
that do not work in Avalonia.
---
## Step 1 — Check before you answer
**NEVER** answer from memory alone for:
- Specific control properties or template part names
- Pseudo-class selectors (`:pointerover`, `:pressed`, `:focus`, etc.)
- Animation API (`Animation`, `KeyFrame`, `Cue`, `Easing` classes)
- `ControlTheme` vs `Style` syntax differences
- Platform-specific behaviors (mobile vs desktop)
- LiveCharts2 or Svg.Skia properties
**Always verify** using one of these sources in order:
1. **Official docs**: `https://docs.avaloniaui.net/docs/reference/controls/{control-name}`
2. **GitHub source** (most reliable for exact property names):
`https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/{ControlName}.cs`
3. **Avalonia samples**: `https://github.com/AvaloniaUI/Avalonia.Samples`
For styling/theming questions also check:
- `https://github.com/AvaloniaUI/Avalonia/tree/master/src/Avalonia.Themes.Fluent/Controls`
---
## Step 2 — Core Avalonia vs WPF differences
These are frequent sources of errors. Apply automatically:
### Styling
```xml
<!-- Avalonia: CSS-like selectors -->
<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Blue"/>
</Style>
<!-- NOT WPF DataTriggers — those do not exist in Avalonia -->
<!-- NOT WPF Triggers — use pseudo-classes instead -->
```
### ControlTheme (Avalonia 11+)
```xml
<!-- For re-theming built-in controls use ControlTheme, not Style -->
<ControlTheme x:Key="{x:Type Button}" TargetType="Button">
<Setter Property="Template">
<ControlTemplate>...</ControlTemplate>
</Setter>
</ControlTheme>
```
### Bindings
```xml
<!-- x:CompileBindings="True" (default) requires x:DataType -->
<!-- Use x:CompileBindings="False" on shell/dynamic views -->
<!-- DynamicResource NOT StaticResource for theme colors -->
<!-- No ElementName binding across UserControl boundaries — use RelativeSource or pass via property -->
```
### No DataTriggers
Avalonia has no DataTriggers. Use instead:
- `Classes.myClass="{Binding SomeBool}"` + style on `.myClass`
- `IsVisible="{Binding SomeBool}"`
- `MultiBinding` with converter
### x:Name in code-behind
`x:Name` does NOT create direct fields in Avalonia. Access named controls via:
```csharp
var btn = this.Get<Button>("PART_Button"); // throws if not found
var btn = this.FindControl<Button>("PART_Button"); // returns null if not found
// TranslateTransform cannot have x:Name — access via RenderTransform:
var tf = (TranslateTransform)someControl.RenderTransform!;
```
### Animations in code-behind
```csharp
var animation = new Animation
{
Duration = TimeSpan.FromMilliseconds(320),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(TranslateTransform.YProperty, 0d) } },
new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(TranslateTransform.YProperty, 300d) } }
}
};
await animation.RunAsync(targetControl);
```
### Platform detection
```csharp
bool isMobile = ApplicationLifetime is ISingleViewApplicationLifetime;
// App.IsMobile is the project's cached version
```
---
## Step 3 — Known Avalonia gotchas from this project
### ViewLocator (no DataTemplates in AXAML)
```csharp
// ViewLocator auto-resolves: {Name}ViewModel → {Name}View (desktop) or {Name}ViewMobile (mobile)
// Do NOT register DataTemplates in AXAML
// Register FuncDataTemplate in App.axaml.cs code-behind if needed
```
### Observable property initialization order
Object initializers set properties one by one — `partial void On{Property}Changed` fires
immediately, before other properties are set. **Never** trigger initialization logic from
property changed handlers when the VM needs multiple properties. Always use an explicit
`Initialize()` method called after the object initializer.
```csharp
// WRONG
partial void OnTransactionsChanged(List<Transaction> value) => ProcessData(); // Categories may be null
// RIGHT
var vm = new MyViewModel { Transactions = t, Categories = c, Accounts = a };
vm.Initialize(); // all props guaranteed set
```
### ObservableCollection mutations
Mutating a `List<T>` never triggers binding updates. Replace the entire collection:
```csharp
MyList = new List<T>(newItems); // triggers OnPropertyChanged
// NOT: MyList.Add(item); // binding won't update for List<T>
```
For `ObservableCollection<T>`, `.Add()` and `.Remove()` do trigger updates but `.Clear()` +
re-add causes a full re-render. Prefer replacing the collection for large updates.
### ScrollViewer + LiveCharts2
LiveCharts2 CartesianChart intercepts scroll events. Forward them manually:
```csharp
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var charts = this.GetVisualDescendants().OfType<CartesianChart>();
foreach (var chart in charts)
chart.AddHandler(PointerWheelChangedEvent, OnChartScroll, RoutingStrategies.Tunnel);
}
private void OnChartScroll(object? sender, PointerWheelEventArgs e)
{
var sv = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();
if (sv is null) return;
sv.Offset = new Vector(sv.Offset.X, sv.Offset.Y - e.Delta.Y * sv.SmallChange.Height * 3);
e.Handled = true;
}
```
### Half-donut chart
```xml
<Border Height="150" ClipToBounds="True">
<lvc:PieChart Series="{Binding ...}" Height="300" Margin="0,0,0,-150"
InitialRotation="-180" MaxAngle="180" LegendPosition="Hidden"
ZoomMode="None"/>
</Border>
```
### Svg.Skia CSS
```xml
<!-- stroke-based (Lucide icons) -->
<Svg Path="../Assets/Icons/name.svg" Css="{DynamicResource SvgBlue}"/>
<!-- SvgBlue resource = "path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #7B9CFF; }" -->
<!-- Fill-based icons use SvgFillBlue etc. -->
```
### Mobile-specific AXAML rules
- No `BoxShadow` — GPU expensive, causes jitter
- No `MinWidth`/`MinHeight` on UserControl root
- Add `Classes="mobile"` to root element for mobile-specific style overrides
- Use `VirtualizingStackPanel` in ItemsControl for long lists
- Page size 10 on mobile vs 25 on desktop
### CalendarDayButton / Calendar
Avalonia's Calendar uses `CalendarDayButton` not `CalendarDayItem`.
Template parts: `PART_MonthView`, `PART_YearView`, `PART_HeaderButton`, `PART_PreviousButton`, `PART_NextButton`.
### FlyoutPresenter
```xml
<!-- Custom transparent flyout presenter must be a ControlTheme in Resources, not Styles -->
<ControlTheme x:Key="TransparentFlyoutPresenter" TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</ControlTheme>
```
### TextBox ghost class
```xml
<!-- Transparent textbox that works in all states -->
<Style Selector="TextBox.ghost">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="FocusAdorner" Value="{x:Null}"/>
</Style>
<Style Selector="TextBox.ghost:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<!-- Also add :focus and :disabled variants -->
```
---
## Step 4 — How to look up unfamiliar Avalonia APIs
### For a control's properties:
```
Fetch: https://docs.avaloniaui.net/docs/reference/controls/{control-name-lowercase}
```
### For template part names (e.g. what's inside a ComboBox):
```
Search GitHub: https://github.com/search?q=repo:AvaloniaUI/Avalonia+PART_+{ControlName}&type=code
Or fetch: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/{ControlName}.axaml
```
### For pseudo-class selectors:
```
Fetch: https://docs.avaloniaui.net/docs/reference/styles/pseudo-classes
```
### For animation classes (Easing, FillMode, etc.):
```
Fetch: https://docs.avaloniaui.net/docs/guides/graphics-and-animations/animation
```
### For ColorPicker internals:
```
Fetch: https://raw.githubusercontent.com/AvaloniaUI/Avalonia/refs/heads/master/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
```
---
## Step 5 — Response format
1. State what you verified and where
2. Provide the correct AXAML or C# with no WPF-isms
3. Flag any Avalonia version caveat if relevant (project uses 11.x)
4. If something cannot be done via AXAML, explain the code-behind approach
5. Never guess at property names — fetch source if uncertain

View File

@@ -0,0 +1,41 @@
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

View File

@@ -19,20 +19,18 @@ jobs:
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore Clario/Clario.csproj
- name: Build
run: dotnet build Clario/Clario.csproj --configuration Release --no-restore
- name: Publish - name: Publish
run: dotnet publish Clario/Clario.csproj \ run: |
--configuration Release \ dotnet publish Clario.Desktop/Clario.Desktop.csproj \
--runtime linux-x64 \ -r linux-x64 \
-c Release \
--self-contained true \ --self-contained true \
--output ./publish/linux \
-p:PublishSingleFile=true \ -p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=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 - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

4
.gitignore vendored
View File

@@ -4,3 +4,7 @@ obj/
.idea/ .idea/
*.user *.user
*.suo *.suo
./Clario/CLAUDE_CONTEXT.md
publish/
*.tar.gz
Clario/devsettings.json

196
CLAUDE.md Normal file
View File

@@ -0,0 +1,196 @@
# Clario — Claude Code Instructions
Clario is a cross-platform personal finance tracking app.
See @NEW_CHAT_CONTEXT.md for full project context before starting any task.
---
## Tech Stack
- **UI**: Avalonia UI XPlat (.NET 9), CommunityToolkit.MVVM
- **Backend**: Supabase (PostgreSQL, Auth, RLS, Realtime)
- **Charts**: LiveCharts2 (SkiaSharp)
- **IDE**: JetBrains Rider, Windows dev machine (Arabic region — always use `en-US` CultureInfo)
## Project Structure
```
Clario/ ← shared (ViewModels, Models, Services, Data, CustomControls, Behaviors, Converters)
Clario.Desktop/ ← Windows/macOS/Linux entry point
Clario.Android/ ← Android entry point
Views/ ← desktop AXAML views only
MobileViews/ ← mobile AXAML views only
```
## Build & Run
```bash
# Desktop
dotnet run --project Clario.Desktop
# Android (requires connected device or emulator)
dotnet build Clario.Android -c Release
# Verify build
dotnet build Clario.sln
```
## Platform Detection
```csharp
// Always check this before any platform-specific logic
App.IsMobile // true on Android/iOS, false on desktop
```
---
## CRITICAL RULES — Read before every task
### AXAML Rules
- **ALWAYS** use `{DynamicResource}` for theme colors, never hardcode hex
- **NEVER** put DataTemplates in AXAML — ViewLocator handles all view resolution
- **NEVER** add `MinWidth`/`MinHeight` to UserControl in mobile views
- **NEVER** use `BoxShadow` in mobile views
- Use `x:CompileBindings="False"` on shell views with dynamic DataContext
- Desktop views go in `Views/`, mobile views go in `MobileViews/` named `{Name}ViewMobile.axaml`
- Icon background opacity always: `<SolidColorBrush Color="..." Opacity="0.15"/>`
- Separator between list items: `Spacing="1"` on StackPanel + `BorderSubtle` background on container
### ViewModel Rules
- **NEVER** fetch data in child ViewModel constructors
- **NEVER** trigger initialization from `partial void On{Property}Changed` when VM depends on multiple properties
- Call `Initialize()` explicitly after object initializer sets all required fields
- Child VMs have `public required ViewModelBase parentViewModel`
- Replace lists entirely to trigger bindings — never mutate and expect updates
### C# Rules
- Always `en-US` CultureInfo for dates/numbers (Windows has Arabic region)
- Use `Task.WhenAll` for parallel async fetches in `InitializeApp`
- Use `_ = SomeAsyncMethod()` for fire-and-forget with try/catch inside the method
- Wrap fire-and-forget in try/catch — exceptions are silently swallowed
### Style Classes
```
accented → primary action button (AccentBlue bg)
base → secondary action button
nav → transparent navigation/toggle button
danger → destructive action (DangerButtonBackground + AccentRed text)
ghost → transparent TextBox (no border, any state)
label → uppercase muted TextBlock label
muted → TextMuted foreground
mobile → root class on mobile views (enables mobile overrides)
```
---
## Design Tokens (quick reference)
```
BgBase/BgSurface/BgSidebar/BgHover
BorderSubtle/BorderAccent
TextPrimary/TextSecondary/TextMuted/TextDisabled
AccentBlue/AccentGreen/AccentYellow/AccentRed/AccentPurple/AccentOrange/AccentPink
IconBgBlue/IconBgGreen/IconBgRed/IconBgOrange/IconBgPurple/IconBgPink
BadgeBgRed/BadgeBgYellow/BadgeBgGreen/BadgeBgBlue
DangerButtonBackground/DangerButtonBorder
SvgPrimary/SvgSecondary/SvgMuted/SvgDisabled/SvgBlue/SvgGreen/SvgYellow/SvgRed
```
## SVG Pattern
```xml
<Svg Path="../Assets/Icons/icon-name.svg" Width="16" Height="16" Css="{DynamicResource SvgBlue}"/>
```
---
## Converters (quick reference)
| Key | Usage |
|-----|-------|
| `HexToColorConverter` | `ConverterParameter=color/css/brush` |
| `AmountColorConverter` | type string → AccentRed/Green brush |
| `AmountSignConverter` | MultiBinding(amount, type) → `+$x.xx` |
| `BoolToColorConverter` | `ConverterParameter='#hex1\|#hex2'` |
| `BoolToCssConverter` | `ConverterParameter='#hex1\|#hex2'` → SVG CSS |
| `SvgPathFromName` | `"icon-name"``"../Assets/Icons/icon-name.svg"` |
| `DateFormatConverter` | DateTime → string (always en-US) |
| `EqualValueConverter` | MultiBinding equality → bool |
| `NetworthSumConverter` | MultiBinding(income, expenses) → net |
| `PercentageConverter` | MultiBinding(value, total) → % |
---
## Flyout Pattern
```xml
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight"
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
<views:SomeView/>
</Flyout>
</Button.Flyout>
```
## Modal Overlay Pattern
```xml
<!-- In MainView content area, on top of ContentControl -->
<views:SomeFormView
DataContext="{Binding SomeFormVM}"
IsVisible="{Binding IsFormVisible}"/>
```
The view's root Grid must have `<Border Background="#70000000"/>` as the dim layer.
## Bottom Sheet Pattern (mobile)
- Controlled via `ShowSheet()` / `HideSheet()` public methods in code-behind
- `TranslateTransform` animation: CubicEaseOut 320ms up, CubicEaseIn 260ms down
- `OverlayGrid.IsVisible = false` by default in AXAML
- Set `BottomSheet.MaxHeight = Bounds.Height * 0.82` in `OnAttachedToVisualTree`
---
## Supabase
```csharp
// All queries via DataRepo
DataRepo.General.FetchTransactions()
DataRepo.General.FetchCategories()
DataRepo.General.FetchAccounts()
DataRepo.General.FetchBudgets()
DataRepo.General.FetchProfileInfo()
// etc.
// Auth
SupabaseService.Client.Auth.CurrentUser
SupabaseService.Client.Auth.SignIn(email, password)
SupabaseService.Client.Auth.SignOut()
SupabaseService.Client.Auth.Update(new UserAttributes { ... })
```
RLS: all tables enabled. INSERT uses `WITH CHECK (auth.uid() = user_id)`.
---
## Verification
After any code change, verify by:
1. `dotnet build Clario.sln` — must have zero errors
2. Check AXAML for `{DynamicResource}` on all color bindings
3. Check that no ViewModel constructor fetches data
4. On mobile views: no `BoxShadow`, no `MinWidth`/`MinHeight`, has `Classes="mobile"` on root
---
## What's Not Yet Built
- `AuthViewMobile` — needs creating
- Settings view mobile version — needs creating
- Analytics view — not designed yet
- Light theme — token file incomplete, do not assume it's complete
- Real-time Supabase subscriptions — not wired to UI

View File

@@ -15,10 +15,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Android"/> <PackageReference Include="Avalonia.Android"/>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.Svg.Skia" /> <PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" /> <PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" /> <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" /> <PackageReference Include="Supabase" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen"/> <PackageReference Include="Xamarin.AndroidX.Core.SplashScreen"/>

View File

@@ -8,15 +8,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Browser"/> <PackageReference Include="Avalonia.Browser"/>
<PackageReference Include="Avalonia.Svg.Skia" /> <PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" /> <PackageReference Include="Avalonia.Svg.Skia"/>
<PackageReference Include="FluentAvalonia.ProgressRing" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" /> <PackageReference Include="FluentAvalonia.ProgressRing"/>
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
<PackageReference Include="Supabase" /> <PackageReference Include="SkiaSharp"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
<PackageReference Include="Supabase"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Clario\Clario.csproj" /> <ProjectReference Include="..\Clario\Clario.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -3,6 +3,7 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -10,6 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.Desktop"/> <PackageReference Include="Avalonia.Desktop"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics"> <PackageReference Include="Avalonia.Diagnostics">
@@ -20,6 +22,7 @@
<PackageReference Include="Deadpikle.AvaloniaProgressRing" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" /> <PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" /> <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" /> <PackageReference Include="Supabase" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,36 @@
{
"GeneralSettings": {
"NetProjectPath": "Clario.Desktop.csproj",
"ApplicationName": "Clario",
"Version": "0.4.0",
"PackageName": {
"$type": "msbuild",
"property": "AssemblyName"
},
"AssemblyName": {
"$type": "msbuild",
"property": "AssemblyName"
}
},
"LinuxSettings": {
"AppIcon": "../Clario/Assets/Logo.png",
"CreateBinSymlink": "True"
},
"Win32Settings": {
"InstallerIcon": "../Clario/Assets/Clario-Logo.svg",
"Company": "Clario",
"IncludeUninstaller": "True"
},
"MacOsSettings": {
"CreateBundle": true,
"BundleIdentifier": "com.CompanyName.Clario-Desktop",
"SigningCredentialsType": "AdHoc"
},
"PublishSettings": {
"PublishSingleFile": "True",
"PublishReadyToRun": "True",
"ExtraBuildProperties": {
"RuntimeFrameworkVersion": "8.0.11"
}
}
}

View File

@@ -7,11 +7,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.iOS"/> <PackageReference Include="Avalonia.iOS"/>
<PackageReference Include="Avalonia.Svg.Skia" /> <PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" /> <PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" /> <PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" /> <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" /> <PackageReference Include="Supabase" />
</ItemGroup> </ItemGroup>

View File

@@ -28,10 +28,13 @@
<converters:DecimalColorConverter x:Key="DecimalColorConverter" /> <converters:DecimalColorConverter x:Key="DecimalColorConverter" />
<converters:BoolToColorConverter x:Key="BoolToColorConverter" /> <converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:BoolToCssConverter x:Key="BoolToCssConverter" /> <converters:BoolToCssConverter x:Key="BoolToCssConverter" />
<converters:CreditAmountConverter x:Key="CreditAmountConverter"/>
<converters:BoolToStringConverter x:Key="BoolToStringConverter"/>
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<StyleInclude Source="../Theme/AppTheme.axaml" /> <StyleInclude Source="../Theme/AppTheme.axaml" />
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/> <StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
</Application.Styles> </Application.Styles>
</Application> </Application>

View File

@@ -42,7 +42,7 @@ public partial class App : Application
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatformLoading) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatformLoading)
{ {
Console.WriteLine("ANDROID PATH HIT"); DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatformLoading.MainView = new MainAppMobile() singleViewPlatformLoading.MainView = new MainAppMobile()
{ {
DataContext = new LoadingViewModel() DataContext = new LoadingViewModel()
@@ -60,8 +60,9 @@ public partial class App : Application
{ {
await SupabaseService.Client.Auth.RetrieveSessionAsync(); await SupabaseService.Client.Auth.RetrieveSessionAsync();
} }
catch catch (Exception e)
{ {
DebugLogger.Log($"[Auth] RetrieveSession failed: {e.Message}");
} }
var user = SupabaseService.Client.Auth.CurrentUser; var user = SupabaseService.Client.Auth.CurrentUser;
@@ -83,7 +84,7 @@ public partial class App : Application
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{ {
Console.WriteLine("ANDROID PATH HIT"); DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel(); singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -1,25 +0,0 @@
<svg width="261" height="293" viewBox="0 0 261 293" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M158.179 13.01C158.66 7.50821 154.588 2.62135 149.066 2.52288C122.238 2.04452 95.7365 9.07331 72.5947 22.9121C49.4528 36.7509 30.7204 56.7719 18.4475 80.6321C15.9214 85.5434 18.2994 91.4436 23.3741 93.6229L52.2522 106.025C57.3268 108.204 63.1612 105.823 65.875 101.013C73.7479 87.058 85.1404 75.3323 98.9894 67.0506C112.838 58.7689 128.559 54.281 144.578 53.9485C150.1 53.8339 154.958 49.8208 155.44 44.319L158.179 13.01Z" fill="url(#paint0_linear_104_135)"/>
<path d="M23.3741 93.6229C18.2994 91.4436 12.3833 93.7818 10.5611 98.9954C-0.408524 130.383 -0.208299 164.717 11.32 196.122C22.8483 227.527 44.9093 253.837 73.5808 270.673C78.3433 273.469 84.3671 271.424 86.8266 266.479L100.822 238.339C103.282 233.394 101.23 227.436 96.5789 224.458C79.6305 213.605 66.5961 197.463 59.5985 178.4C52.6009 159.337 52.0958 138.595 57.9976 119.355C59.6172 114.075 57.3268 108.204 52.2522 106.025L23.3741 93.6229Z" fill="url(#paint1_linear_104_135)"/>
<path d="M86.8266 266.479C84.3671 271.424 86.3711 277.462 91.4748 279.572C119.302 291.079 150.128 293.604 179.597 286.645C209.066 279.685 235.506 263.636 255.245 240.896C258.865 236.725 257.956 230.429 253.544 227.106L228.438 208.201C224.026 204.879 217.793 205.806 214.015 209.834C201.617 223.05 185.555 232.394 167.777 236.593C149.999 240.791 131.453 239.621 114.452 233.347C109.271 231.435 103.282 233.394 100.822 238.339L86.8266 266.479Z" fill="url(#paint2_linear_104_135)"/>
<path d="M254.158 66.7147C258.595 63.4264 259.552 57.1377 255.964 52.9394C244.93 40.0298 231.681 29.153 216.815 20.8348C201.949 12.5165 185.749 6.91511 168.974 4.26464C163.519 3.40269 158.66 7.50821 158.179 13.01L155.44 44.319C154.958 49.8208 159.046 54.6165 164.464 55.6882C173.994 57.5734 183.189 60.9512 191.703 65.7152C200.217 70.4792 207.905 76.5485 214.497 83.6844C218.245 87.7411 224.471 88.716 228.908 85.4277L254.158 66.7147Z" fill="url(#paint3_linear_104_135)"/>
<path d="M52.2522 106.025L23.3741 93.6229M52.2522 106.025C57.3268 108.204 63.1612 105.823 65.875 101.013C73.7479 87.058 85.1404 75.3323 98.9894 67.0506C112.838 58.7689 128.559 54.281 144.578 53.9485C150.1 53.8339 154.958 49.8208 155.44 44.319M52.2522 106.025C57.3268 108.204 59.6172 114.075 57.9976 119.355C52.0958 138.595 52.6009 159.337 59.5985 178.4C66.5961 197.463 79.6305 213.605 96.5789 224.458C101.23 227.436 103.282 233.394 100.822 238.339M23.3741 93.6229C18.2994 91.4436 15.9214 85.5434 18.4475 80.6321C30.7204 56.7719 49.4528 36.7509 72.5947 22.9121C95.7365 9.07331 122.238 2.04452 149.066 2.52288C154.588 2.62135 158.66 7.50821 158.179 13.01M23.3741 93.6229C18.2994 91.4436 12.3833 93.7818 10.5611 98.9954C-0.408524 130.383 -0.208299 164.717 11.32 196.122C22.8483 227.527 44.9093 253.837 73.5808 270.673C78.3433 273.469 84.3671 271.424 86.8266 266.479M155.44 44.319L158.179 13.01M155.44 44.319C154.958 49.8208 159.046 54.6165 164.464 55.6882C173.994 57.5734 183.189 60.9512 191.703 65.7152C200.217 70.4792 207.905 76.5485 214.497 83.6844C218.245 87.7411 224.471 88.716 228.908 85.4277L254.158 66.7147C258.595 63.4264 259.552 57.1377 255.964 52.9394C244.93 40.0298 231.681 29.153 216.815 20.8348C201.949 12.5165 185.749 6.91511 168.974 4.26464C163.519 3.40269 158.66 7.50821 158.179 13.01M100.822 238.339L86.8266 266.479M100.822 238.339C103.282 233.394 109.271 231.435 114.452 233.347C131.453 239.621 149.999 240.791 167.777 236.593C185.555 232.394 201.617 223.05 214.015 209.834C217.793 205.806 224.026 204.879 228.438 208.201L253.544 227.106C257.956 230.429 258.865 236.725 255.245 240.896C235.506 263.636 209.066 279.685 179.597 286.645C150.128 293.604 119.302 291.079 91.4748 279.572C86.3711 277.462 84.3671 271.424 86.8266 266.479" stroke="#13161E" stroke-width="5"/>
<defs>
<linearGradient id="paint0_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint1_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint2_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint3_linear_104_135" x1="263.849" y1="11.0816" x2="206.286" y2="106.27" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

View 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"
>
<rect width="20" height="5" x="2" y="3" rx="1" />
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
<path d="M10 12h4" />
</svg>

After

Width:  |  Height:  |  Size: 340 B

View 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 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>

After

Width:  |  Height:  |  Size: 262 B

View 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="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1 @@
<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" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View 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="m10 17 5-5-5-5" />
<path d="M15 12H3" />
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1 @@
<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" class="lucide lucide-log-out-icon lucide-log-out"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1 @@
<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" class="lucide lucide-refresh-cw-icon lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>

After

Width:  |  Height:  |  Size: 411 B

View 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"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1 @@
<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" class="lucide lucide-upload-icon lucide-upload"><path d="M12 3v12"/><path d="m17 8-5-5-5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View 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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<line x1="19" x2="19" y1="8" y2="14" />
<line x1="22" x2="16" y1="11" y2="11" />
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1 @@
<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" class="lucide lucide-wallet-cards-icon lucide-wallet-cards"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"/><path d="M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21"/></svg>

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,12 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="64" fill="#13161E"/>
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9867 75.7593 45.872C75.7593 42.7574 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0854 96.372 30.896C98.0844 31.7067 99.4757 32.8374 100.546 34.288C101.616 35.696 102.215 37.3174 102.344 39.152V52.592C102.215 54.4267 101.616 56.0694 100.546 57.52C99.5185 58.928 98.1486 60.0374 96.4362 60.848C94.7238 61.6587 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5494 97.7847 51.184C99.069 49.776 99.7111 48.0054 99.7111 45.872C99.7111 44.4214 99.4114 43.1414 98.8121 42.032C98.2556 40.9227 97.4422 40.0694 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0694 87.2536 40.9227 86.6114 42.032C86.0121 43.1414 85.7125 44.4214 85.7125 45.872C85.7125 47.28 86.0121 48.5387 86.6114 49.648C87.2108 50.7574 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5094 123.774 34.6934 125.7 32.688C127.627 30.6827 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9147 137.644 30.384C138.8 30.8107 139.806 31.4934 140.662 32.432L134.819 39.792C134.391 39.3227 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8374 125.7 42.3307 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4294 142.894 22.1494 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1494 152.997 23.4294 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5654 161.714 56.6027 160.215 54.128C158.717 51.6534 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3414 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0774 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6534 189.882 54.128C188.384 56.6027 186.35 58.5654 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9334 178.773 52.336C179.843 51.696 180.657 50.8214 181.213 49.712C181.813 48.56 182.112 47.2587 182.112 45.808C182.112 44.3574 181.813 43.0987 181.213 42.032C180.614 40.9227 179.779 40.0694 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0694 169.484 40.9227 168.884 42.032C168.285 43.1414 167.985 44.4214 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8214 170.318 51.696 171.389 52.336C172.459 52.9334 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.5629C3.26812 20.1135 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1431 -0.560215 35.2232 1.82443 41.6993C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.649 16.9343 57.2274 17.443 56.2077L20.3381 50.4049C20.8468 49.3852 20.4224 48.1565 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0448C10.3634 34.1138 10.2589 29.8367 11.4797 25.8691C11.8148 24.7803 11.341 23.5697 10.2913 23.1203L4.31783 20.5629Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1136 54.1264 10.2527C51.8524 7.60548 49.1219 5.3751 46.0581 3.66936C42.9943 1.96362 39.6554 0.814996 36.1983 0.27149C35.0741 0.0947387 34.0727 0.936616 33.9735 2.06482L33.4089 8.48501C33.3097 9.61322 34.1522 10.5966 35.2688 10.8164C37.2328 11.203 39.1278 11.8956 40.8825 12.8725C42.6372 13.8494 44.2218 15.094 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9147L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,12 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="64" fill="white"/>
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9866 75.7593 45.872C75.7593 42.7573 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0853 96.372 30.896C98.0844 31.7066 99.4757 32.8373 100.546 34.288C101.616 35.696 102.215 37.3173 102.344 39.152V52.592C102.215 54.4266 101.616 56.0693 100.546 57.52C99.5185 58.928 98.1486 60.0373 96.4362 60.848C94.7238 61.6586 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5493 97.7847 51.184C99.069 49.776 99.7111 48.0053 99.7111 45.872C99.7111 44.4213 99.4114 43.1413 98.8121 42.032C98.2556 40.9226 97.4422 40.0693 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0693 87.2536 40.9226 86.6114 42.032C86.0121 43.1413 85.7125 44.4213 85.7125 45.872C85.7125 47.28 86.0121 48.5386 86.6114 49.648C87.2108 50.7573 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5093 123.774 34.6933 125.7 32.688C127.627 30.6826 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9146 137.644 30.384C138.8 30.8106 139.806 31.4933 140.662 32.432L134.819 39.792C134.391 39.3226 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8373 125.7 42.3306 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4293 142.894 22.1493 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1493 152.997 23.4293 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5653 161.714 56.6026 160.215 54.128C158.717 51.6533 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3413 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0773 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6533 189.882 54.128C188.384 56.6026 186.35 58.5653 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9333 178.773 52.336C179.843 51.696 180.657 50.8213 181.213 49.712C181.813 48.56 182.112 47.2586 182.112 45.808C182.112 44.3573 181.813 43.0986 181.213 42.032C180.614 40.9226 179.779 40.0693 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0693 169.484 40.9226 168.884 42.032C168.285 43.1413 167.985 44.4213 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8213 170.318 51.696 171.389 52.336C172.459 52.9333 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.563C3.26812 20.1136 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1432 -0.560215 35.2233 1.82443 41.6994C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.6491 16.9343 57.2275 17.443 56.2077L20.3381 50.405C20.8468 49.3853 20.4224 48.1566 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0449C10.3634 34.1139 10.2589 29.8368 11.4797 25.8692C11.8148 24.7804 11.341 23.5697 10.2913 23.1203L4.31783 20.563Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1137 54.1264 10.2528C51.8524 7.60554 49.1219 5.37516 46.0581 3.66942C42.9943 1.96368 39.6554 0.815057 36.1983 0.271551C35.0741 0.0947997 34.0727 0.936677 33.9735 2.06488L33.4089 8.48507C33.3097 9.61328 34.1522 10.5967 35.2688 10.8165C37.2328 11.203 39.1278 11.8957 40.8825 12.8726C42.6372 13.8495 44.2218 15.0941 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9148L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,11 @@
<svg width="193" height="64" viewBox="0 0 193 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60.9424 61.424V14.96H70.7671V61.424H60.9424Z" fill="#3B6AFF"/>
<path d="M90.7211 62.064C87.8529 62.064 85.2844 61.36 83.0155 59.952C80.7894 58.544 79.0128 56.624 77.6857 54.192C76.4014 51.76 75.7593 48.9866 75.7593 45.872C75.7593 42.7573 76.4014 39.984 77.6857 37.552C79.0128 35.12 80.7894 33.2 83.0155 31.792C85.2844 30.384 87.8529 29.68 90.7211 29.68C92.8188 29.68 94.7024 30.0853 96.372 30.896C98.0844 31.7066 99.4757 32.8373 100.546 34.288C101.616 35.696 102.215 37.3173 102.344 39.152V52.592C102.215 54.4266 101.616 56.0693 100.546 57.52C99.5185 58.928 98.1486 60.0373 96.4362 60.848C94.7238 61.6586 92.8188 62.064 90.7211 62.064ZM92.7118 53.232C94.8094 53.232 96.5004 52.5493 97.7847 51.184C99.069 49.776 99.7111 48.0053 99.7111 45.872C99.7111 44.4213 99.4114 43.1413 98.8121 42.032C98.2556 40.9226 97.4422 40.0693 96.372 39.472C95.3446 38.832 94.1459 38.512 92.776 38.512C91.4061 38.512 90.186 38.832 89.1158 39.472C88.0884 40.0693 87.2536 40.9226 86.6114 42.032C86.0121 43.1413 85.7125 44.4213 85.7125 45.872C85.7125 47.28 86.0121 48.5386 86.6114 49.648C87.2108 50.7573 88.0456 51.632 89.1158 52.272C90.186 52.912 91.3847 53.232 92.7118 53.232ZM99.3258 61.424V53.04L100.803 45.488L99.3258 37.936V30.32H108.958V61.424H99.3258Z" fill="#3B6AFF"/>
<path d="M115.875 61.424V30.32H125.7V61.424H115.875ZM125.7 44.336L121.591 41.136C122.404 37.5093 123.774 34.6933 125.7 32.688C127.627 30.6826 130.302 29.68 133.727 29.68C135.225 29.68 136.531 29.9146 137.644 30.384C138.8 30.8106 139.806 31.4933 140.662 32.432L134.819 39.792C134.391 39.3226 133.855 38.96 133.213 38.704C132.571 38.448 131.843 38.32 131.03 38.32C129.403 38.32 128.098 38.832 127.113 39.856C126.171 40.8373 125.7 42.3306 125.7 44.336Z" fill="#3B6AFF"/>
<path d="M143.279 61.424V30.32H153.104V61.424H143.279ZM148.224 26.032C146.683 26.032 145.398 25.52 144.371 24.496C143.386 23.4293 142.894 22.1493 142.894 20.656C142.894 19.12 143.386 17.84 144.371 16.816C145.398 15.792 146.683 15.28 148.224 15.28C149.765 15.28 151.028 15.792 152.012 16.816C152.997 17.84 153.489 19.12 153.489 20.656C153.489 22.1493 152.997 23.4293 152.012 24.496C151.028 25.52 149.765 26.032 148.224 26.032Z" fill="#3B6AFF"/>
<path d="M175.049 62.128C171.838 62.128 168.927 61.424 166.316 60.016C163.747 58.5653 161.714 56.6026 160.215 54.128C158.717 51.6533 157.968 48.88 157.968 45.808C157.968 42.736 158.717 39.984 160.215 37.552C161.714 35.12 163.747 33.2 166.316 31.792C168.884 30.3413 171.795 29.616 175.049 29.616C178.302 29.616 181.213 30.32 183.782 31.728C186.35 33.136 188.384 35.0773 189.882 37.552C191.381 39.984 192.13 42.736 192.13 45.808C192.13 48.88 191.381 51.6533 189.882 54.128C188.384 56.6026 186.35 58.5653 183.782 60.016C181.213 61.424 178.302 62.128 175.049 62.128ZM175.049 53.232C176.461 53.232 177.703 52.9333 178.773 52.336C179.843 51.696 180.657 50.8213 181.213 49.712C181.813 48.56 182.112 47.2586 182.112 45.808C182.112 44.3573 181.813 43.0986 181.213 42.032C180.614 40.9226 179.779 40.0693 178.709 39.472C177.682 38.832 176.461 38.512 175.049 38.512C173.679 38.512 172.459 38.832 171.389 39.472C170.318 40.0693 169.484 40.9226 168.884 42.032C168.285 43.1413 167.985 44.4213 167.985 45.872C167.985 47.28 168.285 48.56 168.884 49.712C169.484 50.8213 170.318 51.696 171.389 52.336C172.459 52.9333 173.679 53.232 175.049 53.232Z" fill="#3B6AFF"/>
<path d="M18.7486 56.9368C18.24 57.9379 18.6544 59.1603 19.7097 59.5876C25.4636 61.9173 31.8375 62.4285 37.9309 61.0195C44.0243 59.6104 49.4913 56.3611 53.5727 51.757C54.3213 50.9126 54.1334 49.6378 53.2211 48.9652L48.0298 45.1375C47.1176 44.4649 45.8288 44.6527 45.0475 45.4682C42.4841 48.144 39.1628 50.0358 35.4868 50.8859C31.8108 51.7359 27.976 51.4989 24.4608 50.2287C23.3895 49.8416 22.1511 50.2383 21.6425 51.2394L18.7486 56.9368Z" fill="#3B6AFF"/>
<path d="M4.31783 20.563C3.26812 20.1136 2.04436 20.5957 1.66746 21.6708C-0.601631 28.1432 -0.560215 35.2233 1.82443 41.6994C4.20907 48.1754 8.77242 53.6007 14.7032 57.0724C15.6883 57.6491 16.9343 57.2275 17.443 56.2077L20.3381 50.405C20.8468 49.3853 20.4224 48.1566 19.4603 47.5424C15.9545 45.3045 13.2584 41.9758 11.8109 38.0449C10.3634 34.1139 10.2589 29.8368 11.4797 25.8692C11.8148 24.7804 11.341 23.5697 10.2913 23.1203L4.31783 20.563Z" fill="#3B6AFF"/>
<path d="M32.6177 2.18868C32.7167 1.04295 31.8788 0.0252705 30.7428 0.0047656C25.2237 -0.0948531 19.7716 1.36887 15.0107 4.25076C10.2498 7.13264 6.39603 11.302 3.87117 16.2708C3.35146 17.2935 3.84069 18.5222 4.88469 18.9761L10.8257 21.5587C11.8697 22.0126 13.07 21.5167 13.6283 20.515C15.2479 17.609 17.5917 15.1671 20.4408 13.4425C23.2899 11.7178 26.5241 10.7832 29.8196 10.714C30.9556 10.6901 31.9551 9.85441 32.0541 8.70867L32.6177 2.18868Z" fill="#3B6AFF"/>
<path d="M53.7542 13.0775C54.6687 12.4032 54.8659 11.1137 54.1264 10.2528C51.8524 7.60554 49.1219 5.37516 46.0581 3.66942C42.9943 1.96368 39.6554 0.815057 36.1983 0.271551C35.0741 0.0947997 34.0727 0.936677 33.9735 2.06488L33.4089 8.48507C33.3097 9.61328 34.1522 10.5967 35.2688 10.8165C37.2328 11.203 39.1278 11.8957 40.8825 12.8726C42.6372 13.8495 44.2218 15.0941 45.5804 16.5573C46.3528 17.3892 47.6358 17.5891 48.5503 16.9148L53.7542 13.0775Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#0F1117"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#0F1117"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#0F1117"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#0F1117"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#F0F2F8"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#F0F2F8"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#F0F2F8"/>
<path d="M112.475 31.8413C114.197 30.5655 114.568 28.1254 113.176 26.4965C108.896 21.4876 103.757 17.2674 97.9898 14.0399C92.2231 10.8124 85.9388 8.63908 79.4318 7.6107C77.3157 7.27626 75.4308 8.8692 75.2441 11.0039L74.1816 23.1518C73.9948 25.2865 75.5805 27.1472 77.6821 27.5631C81.3789 28.2945 84.9457 29.6051 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4256C98.5445 39.9996 100.959 40.3778 102.681 39.102L112.475 31.8413Z" fill="#F0F2F8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" fill="#13161E"/>
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5655 114.568 28.1254 113.176 26.4965C108.896 21.4876 103.757 17.2674 97.9898 14.0399C92.2231 10.8124 85.9388 8.63908 79.4318 7.6107C77.3157 7.27626 75.4308 8.8692 75.2441 11.0039L74.1816 23.1518C73.9948 25.2865 75.5805 27.1472 77.6821 27.5631C81.3789 28.2945 84.9457 29.6051 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4256C98.5445 39.9996 100.959 40.3778 102.681 39.102L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,7 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" fill="white"/>
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,6 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1441 111.781C46.1901 113.699 46.9675 116.042 48.9472 116.861C59.7416 121.326 71.6993 122.305 83.1308 119.605C94.5622 116.905 104.818 110.678 112.475 101.854C113.88 100.236 113.527 97.7931 111.816 96.5041L102.077 89.1687C100.365 87.8797 97.9475 88.2395 96.4818 89.8024C91.6727 94.9302 85.4418 98.5558 78.5455 100.185C71.6492 101.814 64.455 101.36 57.8605 98.9255C55.8506 98.1836 53.5273 98.9438 52.5733 100.862L47.1441 111.781Z" fill="#3B6AFF"/>
<path d="M21.0973 43.9209C19.1287 43.0753 16.8338 43.9825 16.127 46.0054C11.8718 58.1837 11.9494 71.5054 16.4214 83.6906C20.8933 95.8758 29.451 106.084 40.573 112.616C42.4204 113.701 44.7571 112.908 45.7111 110.989L51.1403 100.071C52.0943 98.1522 51.2984 95.8403 49.4942 94.6848C42.9197 90.474 37.8636 84.2108 35.1491 76.8144C32.4346 69.418 32.2387 61.3702 34.5281 53.9049C35.1564 51.8563 34.2679 49.5784 32.2994 48.7328L21.0973 43.9209Z" fill="#3B6AFF"/>
<path d="M73.8169 11.1106C74.0037 8.9759 72.4239 7.0798 70.2818 7.04159C59.8752 6.85599 49.595 9.58316 40.618 14.9526C31.641 20.3221 24.3745 28.0902 19.6138 37.348C18.6338 39.2536 19.5563 41.5428 21.5248 42.3884L32.7269 47.2003C34.6954 48.0459 36.9587 47.122 38.0114 45.2557C41.0653 39.8412 45.4846 35.2916 50.8568 32.0784C56.2289 28.8651 62.3272 27.1238 68.5411 26.9947C70.683 26.9503 72.5677 25.3932 72.7544 23.2585L73.8169 11.1106Z" fill="#3B6AFF"/>
<path d="M112.475 31.8413C114.197 30.5654 114.568 28.1254 113.176 26.4964C108.896 21.4875 103.757 17.2673 97.9898 14.0399C92.2231 10.8124 85.9388 8.63902 79.4318 7.61064C77.3157 7.2762 75.4308 8.86914 75.2441 11.0039L74.1816 23.1517C73.9948 25.2864 75.5805 27.1472 77.6821 27.563C81.3789 28.2944 84.9457 29.605 88.2484 31.4535C91.551 33.3019 94.5336 35.6568 97.0907 38.4255C98.5445 39.9995 100.959 40.3778 102.681 39.1019L112.475 31.8413Z" fill="#3B6AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -1,49 +0,0 @@
<svg width="800" height="400" viewBox="0 0 800 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M331.8 292V146.8H362.4V292H331.8Z" fill="url(#paint0_linear_104_124)"/>
<path d="M424.548 294C415.615 294 407.615 291.8 400.548 287.4C393.615 283 388.082 277 383.948 269.4C379.948 261.8 377.948 253.133 377.948 243.4C377.948 233.667 379.948 225 383.948 217.4C388.082 209.8 393.615 203.8 400.548 199.4C407.615 195 415.615 192.8 424.548 192.8C431.082 192.8 436.948 194.067 442.148 196.6C447.482 199.133 451.815 202.667 455.148 207.2C458.482 211.6 460.348 216.667 460.748 222.4V264.4C460.348 270.133 458.482 275.267 455.148 279.8C451.948 284.2 447.682 287.667 442.348 290.2C437.015 292.733 431.082 294 424.548 294ZM430.748 266.4C437.282 266.4 442.548 264.267 446.548 260C450.548 255.6 452.548 250.067 452.548 243.4C452.548 238.867 451.615 234.867 449.748 231.4C448.015 227.933 445.482 225.267 442.148 223.4C438.948 221.4 435.215 220.4 430.948 220.4C426.682 220.4 422.882 221.4 419.548 223.4C416.348 225.267 413.748 227.933 411.748 231.4C409.882 234.867 408.948 238.867 408.948 243.4C408.948 247.8 409.882 251.733 411.748 255.2C413.615 258.667 416.215 261.4 419.548 263.4C422.882 265.4 426.615 266.4 430.748 266.4ZM451.348 292V265.8L455.948 242.2L451.348 218.6V194.8H481.348V292H451.348Z" fill="url(#paint1_linear_104_124)"/>
<path d="M502.894 292V194.8H533.494V292H502.894ZM533.494 238.6L520.694 228.6C523.227 217.267 527.494 208.467 533.494 202.2C539.494 195.933 547.827 192.8 558.494 192.8C563.16 192.8 567.227 193.533 570.694 195C574.294 196.333 577.427 198.467 580.094 201.4L561.894 224.4C560.56 222.933 558.894 221.8 556.894 221C554.894 220.2 552.627 219.8 550.094 219.8C545.027 219.8 540.96 221.4 537.894 224.6C534.96 227.667 533.494 232.333 533.494 238.6Z" fill="url(#paint2_linear_104_124)"/>
<path d="M588.245 292V194.8H618.845V292H588.245ZM603.645 181.4C598.845 181.4 594.845 179.8 591.645 176.6C588.579 173.267 587.045 169.267 587.045 164.6C587.045 159.8 588.579 155.8 591.645 152.6C594.845 149.4 598.845 147.8 603.645 147.8C608.445 147.8 612.379 149.4 615.445 152.6C618.512 155.8 620.045 159.8 620.045 164.6C620.045 169.267 618.512 173.267 615.445 176.6C612.379 179.8 608.445 181.4 603.645 181.4Z" fill="url(#paint3_linear_104_124)"/>
<path d="M687.194 294.2C677.194 294.2 668.127 292 659.994 287.6C651.994 283.067 645.66 276.933 640.994 269.2C636.327 261.467 633.994 252.8 633.994 243.2C633.994 233.6 636.327 225 640.994 217.4C645.66 209.8 651.994 203.8 659.994 199.4C667.994 194.867 677.06 192.6 687.194 192.6C697.327 192.6 706.394 194.8 714.394 199.2C722.394 203.6 728.727 209.667 733.394 217.4C738.06 225 740.394 233.6 740.394 243.2C740.394 252.8 738.06 261.467 733.394 269.2C728.727 276.933 722.394 283.067 714.394 287.6C706.394 292 697.327 294.2 687.194 294.2ZM687.194 266.4C691.594 266.4 695.46 265.467 698.794 263.6C702.127 261.6 704.66 258.867 706.394 255.4C708.26 251.8 709.194 247.733 709.194 243.2C709.194 238.667 708.26 234.733 706.394 231.4C704.527 227.933 701.927 225.267 698.594 223.4C695.394 221.4 691.594 220.4 687.194 220.4C682.927 220.4 679.127 221.4 675.794 223.4C672.46 225.267 669.86 227.933 667.994 231.4C666.127 234.867 665.194 238.867 665.194 243.4C665.194 247.8 666.127 251.8 667.994 255.4C669.86 258.867 672.46 261.6 675.794 263.6C679.127 265.467 682.927 266.4 687.194 266.4Z" fill="url(#paint4_linear_104_124)"/>
<path d="M142.075 320.852C139.663 325.703 141.628 331.626 146.634 333.697C173.927 344.985 204.162 347.462 233.066 340.635C261.97 333.807 287.902 318.062 307.262 295.753C310.813 291.661 309.922 285.484 305.595 282.225L280.97 263.678C276.643 260.419 270.529 261.329 266.823 265.28C254.664 278.246 238.909 287.413 221.472 291.532C204.035 295.651 185.845 294.502 169.171 288.348C164.089 286.472 158.214 288.394 155.802 293.245L142.075 320.852Z" fill="url(#paint5_linear_104_124)"/>
<path d="M76.2161 149.27C71.2388 147.132 65.4361 149.426 63.6489 154.541C52.8897 185.333 53.0861 219.017 64.3932 249.827C75.7004 280.637 97.3382 306.447 125.46 322.964C130.131 325.708 136.039 323.702 138.451 318.85L152.179 291.244C154.591 286.392 152.579 280.547 148.017 277.625C131.393 266.978 118.609 251.142 111.746 232.44C104.882 213.739 104.387 193.39 110.175 174.515C111.764 169.335 109.518 163.575 104.54 161.437L76.2161 149.27Z" fill="url(#paint6_linear_104_124)"/>
<path d="M209.516 66.3108C209.988 60.9133 205.994 56.119 200.578 56.0225C174.265 55.5532 148.272 62.4487 125.574 76.0252C102.876 89.6016 84.5026 109.243 72.4651 132.651C69.9874 137.469 72.3198 143.258 77.2972 145.396L105.621 157.562C110.599 159.7 116.321 157.364 118.983 152.645C126.705 138.955 137.879 127.452 151.462 119.327C165.045 111.202 180.464 106.8 196.176 106.473C201.592 106.361 206.357 102.424 206.829 97.0263L209.516 66.3108Z" fill="url(#paint7_linear_104_124)"/>
<path d="M307.263 118.728C311.615 115.502 312.553 109.332 309.034 105.213C298.212 92.5486 285.217 81.878 270.636 73.7174C256.055 65.5569 240.165 60.0616 223.713 57.4614C218.362 56.6158 213.597 60.6435 213.124 66.041L210.438 96.7565C209.966 102.154 213.975 106.859 219.289 107.91C228.636 109.76 237.655 113.073 246.005 117.747C254.356 122.421 261.897 128.375 268.363 135.376C272.038 139.356 278.145 140.312 282.497 137.086L307.263 118.728Z" fill="url(#paint8_linear_104_124)"/>
<defs>
<linearGradient id="paint0_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint1_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint2_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint3_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint4_linear_104_124" x1="745" y1="91.9999" x2="321" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint5_linear_104_124" x1="315.701" y1="70.2952" x2="259.223" y2="163.668" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint6_linear_104_124" x1="312.078" y1="68.2937" x2="255.6" y2="161.666" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint7_linear_104_124" x1="313.159" y1="64.4189" x2="256.681" y2="157.791" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
<linearGradient id="paint8_linear_104_124" x1="316.768" y1="64.1491" x2="260.29" y2="157.521" gradientUnits="userSpaceOnUse">
<stop stop-color="#7B9CFF"/>
<stop offset="1" stop-color="#3B6AFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -13,6 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia"/> <PackageReference Include="Avalonia"/>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.Svg.Skia" /> <PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Avalonia.Themes.Fluent"/> <PackageReference Include="Avalonia.Themes.Fluent"/>
<PackageReference Include="Avalonia.Fonts.Inter"/> <PackageReference Include="Avalonia.Fonts.Inter"/>
@@ -29,6 +30,9 @@
<PackageReference Include="Supabase" /> <PackageReference Include="Supabase" />
<PackageReference Include="Xaml.Behaviors.Interactions" /> <PackageReference Include="Xaml.Behaviors.Interactions" />
<PackageReference Include="Xaml.Behaviors.Interactivity" /> <PackageReference Include="Xaml.Behaviors.Interactivity" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -36,5 +40,14 @@
<DependentUpon>MobileMainView.axaml</DependentUpon> <DependentUpon>MobileMainView.axaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Update="Views\AccountFormView.axaml.cs">
<DependentUpon>AccountFormView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="devsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,20 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Clario.Converters;
public class BoolToStringConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not bool b || parameter is not string s) return null;
var results = s.Split('|');
return b ? results[0] : results[1];
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Clario.Converters;
public class CreditAmountConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not decimal amount) return 0;
return amount < 0 ? amount * -1 : 0;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Controls.Converters;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
@@ -21,6 +22,18 @@ public class HexToColorConverter : IValueConverter
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
throw new NotImplementedException(); if (parameter is not string type) return null;
var color = Color.Parse("#ffffff");
if (value is Color c)
{
color = c;
}
if (value is SolidColorBrush b)
{
color = b.Color;
}
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
} }
} }

View File

@@ -10,17 +10,19 @@ public class NetworthSumConverter : IMultiValueConverter
{ {
public object? Convert(IList<object?>? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(IList<object?>? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (value is null || value.Count < 2 || value.Any(x => x is null)) return 0; if (value is null || value.Count < 2) return 0;
if (value[0] is double incomeD && (value[1] is double expenseD)) var symbol = value.Count >= 3 && value[2] is string s ? s : "$";
if (value[0] is double incomeD && value[1] is double expenseD)
{ {
var net = incomeD - expenseD; var net = incomeD - expenseD;
return (net < 0 ? $"- ${Math.Abs(net):F2}" : $"+ ${Math.Abs(net):F2}"); return net < 0 ? $"- {symbol}{Math.Abs(net):F2}" : $"+ {symbol}{Math.Abs(net):F2}";
} }
if (value[0] is decimal incomeDec && (value[1] is decimal expenseDec)) if (value[0] is decimal incomeDec && value[1] is decimal expenseDec)
{ {
var net = incomeDec - expenseDec; var net = incomeDec - expenseDec;
return (net < 0 ? $"-${Math.Abs(net):F2}" : $"+${Math.Abs(net):F2}"); return net < 0 ? $"-{symbol}{Math.Abs(net):F2}" : $"+{symbol}{Math.Abs(net):F2}";
} }
return 0; return 0;

View File

@@ -2,6 +2,76 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clario.CustomControls"> xmlns:local="clr-namespace:Clario.CustomControls">
<Styles.Resources>
<!--
Full ControlTheme replacement for CalendarButton.
This is necessary because in Avalonia 11 the Fluent theme owns
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" />
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="Root"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="4">
<ContentControl x:Name="Content"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}" />
</Border>
</ControlTemplate>
</Setter>
<!-- Hover -->
<Style Selector="^:pointerover /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
</Style>
<Style Selector="^:pointerover /template/ ContentControl#Content">
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
</Style>
<!-- Pressed -->
<Style Selector="^:pressed /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
</Style>
<!-- Selected (current month/year) -->
<Style Selector="^:selected /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource BorderAccent}" />
</Style>
<Style Selector="^:selected /template/ ContentControl#Content">
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
</Style>
<!-- Selected + hover -->
<Style Selector="^:selected:pointerover /template/ Border#Root">
<Setter Property="Background" Value="{DynamicResource BorderAccent}" />
</Style>
<!-- Inactive (out-of-range months/years) -->
<Style Selector="^:inactive /template/ ContentControl#Content">
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
</Style>
</ControlTheme>
</Styles.Resources>
<!-- ============================================================ -->
<!-- DateRangePicker control template -->
<!-- ============================================================ -->
<Style Selector="local|DateRangePicker"> <Style Selector="local|DateRangePicker">
<Setter Property="MinHeight" Value="15" /> <Setter Property="MinHeight" Value="15" />
<Setter Property="MinWidth" Value="50" /> <Setter Property="MinWidth" Value="50" />
@@ -14,8 +84,6 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>
<!-- Trigger button -->
<Button x:Name="PART_Button" <Button x:Name="PART_Button"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
@@ -48,7 +116,6 @@
</Grid> </Grid>
</Button> </Button>
<!-- Popup -->
<Popup x:Name="PART_Popup" <Popup x:Name="PART_Popup"
PlacementTarget="{Binding #PART_Button}" PlacementTarget="{Binding #PART_Button}"
Placement="Bottom" Placement="Bottom"
@@ -65,21 +132,50 @@
BorderThickness="0" /> BorderThickness="0" />
</Border> </Border>
</Popup> </Popup>
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
<!-- pointerover -->
<Style Selector="local|DateRangePicker:pointerover /template/ Button#PART_Button"> <Style Selector="local|DateRangePicker:pointerover /template/ Button#PART_Button">
<Setter Property="Background" Value="{DynamicResource BgHover}" /> <Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}" /> <Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}" />
</Style> </Style>
<!-- pressed -->
<Style Selector="local|DateRangePicker:pressed /template/ Button#PART_Button"> <Style Selector="local|DateRangePicker:pressed /template/ Button#PART_Button">
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" /> <Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
</Style> </Style>
<!-- ============================================================ -->
<!-- CalendarItem: nav header buttons (prev / title / next) -->
<!-- ============================================================ -->
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="CalendarItem /template/ Button#PART_HeaderButton:pointerover">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
</Style>
<Style Selector="CalendarItem /template/ Button#PART_PreviousButton">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
</Style>
<Style Selector="CalendarItem /template/ Button#PART_PreviousButton:pointerover">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
</Style>
<Style Selector="CalendarItem /template/ Button#PART_NextButton">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}" />
</Style>
<Style Selector="CalendarItem /template/ Button#PART_NextButton:pointerover">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}" />
</Style>
</Styles> </Styles>

View File

@@ -5,8 +5,10 @@ using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Calendar = Avalonia.Controls.Calendar; using Calendar = Avalonia.Controls.Calendar;
namespace Clario.CustomControls; namespace Clario.CustomControls;
@@ -23,19 +25,28 @@ public class DateRangePicker : TemplatedControl
set => SetValue(SelectionModeProperty, value); set => SetValue(SelectionModeProperty, value);
} }
public static readonly StyledProperty<IList<DateTime>> SelectedDatesProperty = // FIX: Use DirectProperty to avoid shared-instance default and get proper TwoWay support
AvaloniaProperty.Register<DateRangePicker, IList<DateTime>>( private IList<DateTime> _selectedDates = new List<DateTime>();
nameof(SelectedDates), new List<DateTime>());
public static readonly DirectProperty<DateRangePicker, IList<DateTime>> SelectedDatesProperty =
AvaloniaProperty.RegisterDirect<DateRangePicker, IList<DateTime>>(
nameof(SelectedDates),
o => o.SelectedDates,
(o, v) => o.SelectedDates = v,
defaultBindingMode: BindingMode.TwoWay);
public IList<DateTime> SelectedDates public IList<DateTime> SelectedDates
{ {
get => GetValue(SelectedDatesProperty); get => _selectedDates;
set => SetValue(SelectedDatesProperty, value); set => SetAndRaise(SelectedDatesProperty, ref _selectedDates, value);
} }
// FIX: Add defaultBindingMode: TwoWay so changes propagate back to the ViewModel
public static readonly StyledProperty<DateTime?> SelectedDateProperty = public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>( AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedDate), null); nameof(SelectedDate),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay);
public DateTime? SelectedDate public DateTime? SelectedDate
{ {
@@ -58,7 +69,6 @@ public class DateRangePicker : TemplatedControl
private Popup? _popup; private Popup? _popup;
private Calendar? _calendar; private Calendar? _calendar;
private bool _isSyncing = false; private bool _isSyncing = false;
@@ -66,12 +76,12 @@ public class DateRangePicker : TemplatedControl
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
if (_button != null) _button.Click -= OnButtonClick; if (_button != null) _button.Click -= OnButtonClick;
if (_calendar != null) if (_calendar != null)
{ {
_calendar.SelectedDatesChanged -= OnCalendarDatesChanged; _calendar.SelectedDatesChanged -= OnCalendarDatesChanged;
_calendar.RemoveHandler(PointerReleasedEvent, OnCalendarPointerReleased); _calendar.RemoveHandler(PointerReleasedEvent, OnCalendarPointerReleased);
// _calendar.RemoveHandler(Button.ClickEvent, OnCalendarInternalClick); // add this
} }
_button = e.NameScope.Find<Button>("PART_Button"); _button = e.NameScope.Find<Button>("PART_Button");
@@ -85,10 +95,9 @@ public class DateRangePicker : TemplatedControl
{ {
_calendar.AllowTapRangeSelection = true; _calendar.AllowTapRangeSelection = true;
_calendar.SelectedDatesChanged += OnCalendarDatesChanged; _calendar.SelectedDatesChanged += OnCalendarDatesChanged;
_calendar.AddHandler(PointerReleasedEvent, OnCalendarPointerReleased, RoutingStrategies.Tunnel); _calendar.AddHandler(PointerReleasedEvent, OnCalendarPointerReleased, RoutingStrategies.Tunnel);
// _calendar.AddHandler(Button.ClickEvent, OnCalendarInternalClick, RoutingStrategies.Tunnel);
SyncToCalendar(); SyncToCalendar();
} }
@@ -96,27 +105,32 @@ public class DateRangePicker : TemplatedControl
UpdateDisplayText(); UpdateDisplayText();
} }
private void OnCalendarInternalClick(object? sender, RoutedEventArgs e)
{
e.Handled = true;
}
private void OnCalendarPointerReleased(object? sender, PointerReleasedEventArgs e) private void OnCalendarPointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
if (_calendar!.SelectionMode != CalendarSelectionMode.SingleDate) return; if (_calendar!.SelectionMode != CalendarSelectionMode.SingleDate) return;
if (_isSyncing) return; if (_isSyncing) return;
if (_popup is null || !_popup.IsOpen) 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)
return;
var newDates = _calendar!.SelectedDates.OrderBy(d => d).ToList(); var newDates = _calendar!.SelectedDates.OrderBy(d => d).ToList();
_isSyncing = true; _isSyncing = true;
try try
{ {
SelectedDates = newDates; SelectedDates = newDates;
SelectedDate = newDates.Count > 0 ? newDates[0] : null; SelectedDate = newDates.Count > 0 ? newDates[0] : null;
UpdateDisplayText(); UpdateDisplayText();
bool shouldClose = SelectionMode switch bool shouldClose = SelectionMode switch
{ {
CalendarSelectionMode.SingleDate => newDates.Count >= 1, CalendarSelectionMode.SingleDate => newDates.Count >= 1,
@@ -133,12 +147,10 @@ public class DateRangePicker : TemplatedControl
} }
} }
private void OnButtonClick(object? sender, RoutedEventArgs e) private void OnButtonClick(object? sender, RoutedEventArgs e)
{ {
if (_popup is null) return; if (_popup is null) return;
SyncToCalendar(); SyncToCalendar();
_popup.IsOpen = true; _popup.IsOpen = true;
} }
@@ -157,13 +169,10 @@ public class DateRangePicker : TemplatedControl
try try
{ {
SelectedDates = newDates; SelectedDates = newDates;
SelectedDate = newDates.Count > 0 ? newDates[0] : null; SelectedDate = newDates.Count > 0 ? newDates[0] : null;
UpdateDisplayText(); UpdateDisplayText();
bool shouldClose = SelectionMode switch bool shouldClose = SelectionMode switch
{ {
CalendarSelectionMode.SingleDate => newDates.Count >= 1, CalendarSelectionMode.SingleDate => newDates.Count >= 1,
@@ -180,7 +189,6 @@ public class DateRangePicker : TemplatedControl
} }
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
@@ -220,7 +228,6 @@ public class DateRangePicker : TemplatedControl
} }
} }
private void SyncToCalendar() private void SyncToCalendar()
{ {
if (_calendar is null || _isSyncing) return; if (_calendar is null || _isSyncing) return;

View File

@@ -1,49 +1,72 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media.Imaging;
using Clario.Models; using Clario.Models;
using Clario.Models.GeneralModels; using Clario.Models.GeneralModels;
using Clario.Services; using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Clario.Messages;
using CommunityToolkit.Mvvm.Messaging;
using Supabase.Postgrest;
using FileOptions = Supabase.Storage.FileOptions;
namespace Clario.Data; namespace Clario.Data;
public class GeneralDataRepo public record ProfileUpdated();
public partial class GeneralDataRepo : ObservableObject
{ {
public Profile? Profile { get; set; } [ObservableProperty] private Profile? _profile;
public List<Category>? Categories { get; set; } [ObservableProperty] private ObservableCollection<Category> _categories = new();
public List<Account>? Accounts { get; set; } [ObservableProperty] private ObservableCollection<Account> _accounts = new();
public List<Budget>? Budgets { get; set; } [ObservableProperty] private ObservableCollection<Budget> _budgets = new();
public List<Transaction>? Transactions { get; set; } [ObservableProperty] private ObservableCollection<Transaction> _transactions = new();
public async Task<Profile?> FetchProfileInfo() private static readonly HttpClient _HttpClient = new();
private const string Bucket = "avatars";
private const string ProjectRef = "xzxstbllaivumhtpctmo";
private const string PublicBaseUrl = $"https://{ProjectRef}.supabase.co/storage/v1/object/public/{Bucket}";
partial void OnProfileChanged(Profile? value)
{ {
if (Profile is not null) return Profile; _ = GetAvatarFromUrl(value?.AvatarUrl);
}
public async Task<Profile?> FetchProfileInfo(bool forceRefresh = false)
{
if (Profile is not null && !forceRefresh) return Profile;
var profile = await SupabaseService.Client.From<Profile>().Get(); var profile = await SupabaseService.Client.From<Profile>().Get();
if (profile.Models.Count == 0) return null;
Profile = profile.Model; Profile = profile.Model;
return profile.Model;
return Profile;
} }
public async Task InsertProfileInfo(Profile profile) private async Task GetAvatarFromUrl(string? url)
{ {
try if (!string.IsNullOrEmpty(url))
{ {
await SupabaseService.Client.From<Profile>().Insert(profile); var bytes = await _HttpClient.GetByteArrayAsync(url);
} var stream = new MemoryStream(bytes);
catch (Exception e) Profile.Avatar = new Bitmap(stream);
{
Console.WriteLine(e);
return;
} }
Profile = profile; WeakReferenceMessenger.Default.Send(new ProfileUpdated());
} }
public async Task<List<Transaction>> FetchTransactions()
public async Task<List<Transaction>> FetchTransactions(bool forceRefresh = false)
{ {
if (Transactions is not null) return Transactions; if (Transactions.Count != 0 && !forceRefresh) return Transactions.ToList();
var transactions = await SupabaseService.Client.From<Transaction>().Get(); var transactions = await SupabaseService.Client.From<Transaction>().Get();
Transactions = transactions.Models; Transactions = new ObservableCollection<Transaction>(transactions.Models);
return transactions.Models; return transactions.Models;
} }
@@ -51,11 +74,19 @@ public class GeneralDataRepo
{ {
try try
{ {
await SupabaseService.Client.From<Transaction>().Insert(transaction); var result = await SupabaseService.Client.From<Transaction>().Insert(transaction);
if (result.Models.Count >= 1)
{
var resultItem = LinkTransactionCategories(result.Models[0]);
LinkTransactionAccounts(resultItem);
Transactions.Add(resultItem);
}
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
return;
} }
} }
@@ -63,11 +94,22 @@ public class GeneralDataRepo
{ {
try try
{ {
await SupabaseService.Client.From<Transaction>().Update(transaction); var result = await SupabaseService.Client.From<Transaction>().Update(transaction);
if (result.Model is null) return;
var item = Transactions.FirstOrDefault(x => x.Id == result.Model.Id);
if (item is null) return;
var index = Transactions.IndexOf(item);
if (index != -1)
{
var enriched = LinkTransactionCategories(result.Model);
LinkTransactionAccounts(enriched);
Transactions[index] = enriched;
}
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
} }
} }
@@ -76,68 +118,75 @@ public class GeneralDataRepo
try try
{ {
await SupabaseService.Client.From<Transaction>().Where(x => x.Id == id).Delete(); await SupabaseService.Client.From<Transaction>().Where(x => x.Id == id).Delete();
var item = Transactions.FirstOrDefault(x => x.Id == id);
if (item is null) return;
Transactions.Remove(item);
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
throw; throw;
} }
} }
public async Task<List<Category>> FetchCategories() public async Task<List<Category>> FetchCategories(bool forceRefresh = false)
{ {
if (Categories is not null) return Categories; if (Categories.Count != 0 && !forceRefresh) return Categories.ToList();
var categories = await SupabaseService.Client.From<Category>().Get(); var categories = await SupabaseService.Client.From<Category>().Get();
Categories = categories.Models; Categories = new ObservableCollection<Category>(categories.Models);
return categories.Models; return categories.Models;
} }
public async Task<List<Account>> FetchAccounts() public async Task<List<Account>> FetchAccounts(bool forceRefresh = false)
{ {
if (Accounts is not null) return Accounts; if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
var accounts = await SupabaseService.Client.From<Account>().Get(); var accounts = await SupabaseService.Client.From<Account>().Get();
Accounts = accounts.Models; Accounts = new ObservableCollection<Account>(accounts.Models);
return accounts.Models; return accounts.Models.OrderBy(x=>x.IsPrimary).ThenBy(x=>x.CreatedAt).ToList();
} }
public async Task<List<Budget>> FetchBudgets() public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
{ {
if (Budgets is not null) return Budgets; if (Budgets.Count != 0 && !forceRefresh) return Budgets.ToList();
var budgets = await SupabaseService.Client.From<Budget>().Get(); var budgets = await SupabaseService.Client.From<Budget>().Get();
Budgets = budgets.Models; Budgets = new ObservableCollection<Budget>(budgets.Models);
return budgets.Models; return budgets.Models;
} }
public async Task<List<Budget>> FetchProcessedBudgets(DateTime CurrentPeriod) public async Task<List<Budget>> FetchProcessedBudgets(DateTime CurrentPeriod)
{ {
var categories = await FetchCategories(); var budgets = Budgets;
var transactions = await FetchTransactions();
var budgets = await FetchBudgets();
var outputList = new List<Budget>(); var outputList = new List<Budget>();
var primarySymbol = CurrencyService.GetSymbol(PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD");
foreach (var budget in budgets) foreach (var budget in budgets)
{ {
budget.Category = categories.FirstOrDefault(x => x.Id == budget.CategoryId); budget.Category = Categories.FirstOrDefault(x => x.Id == budget.CategoryId);
budget.PrimarySymbol = primarySymbol;
switch (budget.Period.ToLower()) switch (budget.Period.ToLower())
{ {
case "monthly": case "monthly":
var budgetTransactions = transactions.Where(x => var budgetTransactions = Transactions.Where(x =>
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList(); x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = budgetTransactions.Sum(x => x.Amount); budget.Spent = budgetTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = budgetTransactions.Count; budget.TransactionsCount = budgetTransactions.Count;
break; break;
case "quarterly": case "quarterly":
var quarterTransactions = transactions.Where(x => var quarterTransactions = Transactions.Where(x =>
x.Date.Month >= CurrentPeriod.Month - 3 && x.Date.Month <= CurrentPeriod.Month && x.CategoryId == budget.CategoryId).ToList(); x.Date.Month >= CurrentPeriod.Month - 3 && x.Date.Month <= CurrentPeriod.Month && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = quarterTransactions.Sum(x => x.Amount); budget.Spent = quarterTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = quarterTransactions.Count; budget.TransactionsCount = quarterTransactions.Count;
break; break;
case "yearly": case "yearly":
var yearTransactions = transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList(); var yearTransactions = Transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = yearTransactions.Sum(x => x.Amount); budget.Spent = yearTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = yearTransactions.Count; budget.TransactionsCount = yearTransactions.Count;
break; break;
} }
OnPropertyChanged(nameof(budget.IsOnTrack));
OnPropertyChanged(nameof(budget.IsWarning));
OnPropertyChanged(nameof(budget.IsOverBudget));
} }
@@ -173,4 +222,310 @@ public class GeneralDataRepo
return outputList; return outputList;
} }
public async Task<Account?> InsertAccount(Account account)
{
try
{
var result = await SupabaseService.Client.From<Account>()
.Insert(account, new QueryOptions() { Returning = QueryOptions.ReturnType.Representation });
if (result.Model is null) return null;
Accounts.Add(result.Model);
return result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
return null;
}
}
public async Task UpdateAccount(Account account)
{
try
{
var result = await SupabaseService.Client.From<Account>().Update(account);
if (result.Model is null) return;
var item = Accounts.FirstOrDefault(x => x.Id == result.Model.Id);
if (item is null) return;
var index = Accounts.IndexOf(item);
if (index != -1) Accounts[index] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
public async Task MigrateTransactions(Guid accountId, Guid targetAccountId)
{
try
{
var update = await SupabaseService.Client
.From<Transaction>()
.Where(x => x.AccountId == accountId)
.Set(x => x.AccountId, targetAccountId)
.Update();
foreach (var updateModel in update.Models)
{
var item = Transactions.SingleOrDefault(x => x.Id == updateModel.Id);
if (item is null) return;
var index = Transactions.IndexOf(item);
if (index != -1) Transactions[index] = updateModel;
}
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task RecalculateAccountBalance(Guid targetAccountId)
{
var accountResult = Accounts
.SingleOrDefault(a => a.Id == targetAccountId);
if (accountResult is null) return;
var transactionsResult = Transactions
.Where(t => t.AccountId == targetAccountId);
var balance = accountResult.OpeningBalance +
transactionsResult.Sum(t =>
t.Type == "income" ? t.Amount : -t.Amount);
accountResult.CurrentBalance = balance;
await SupabaseService.Client
.From<Account>()
.Update(accountResult);
var index = Accounts.IndexOf(accountResult);
if (index != -1) Accounts[index] = accountResult;
}
public async Task DeleteAccount(Guid accountId)
{
await SupabaseService.Client
.From<Account>()
.Where(a => a.Id == accountId)
.Delete();
var item = Accounts.FirstOrDefault(x => x.Id == accountId);
if (item is null) return;
Accounts.Remove(item);
}
public async Task InsertBudget(Budget budget)
{
try
{
var result = await SupabaseService.Client.From<Budget>().Insert(budget);
if (result.Models.Count >= 1)
{
Budgets.Add(result.Models[0]);
}
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task UpdateBudget(Budget budget)
{
try
{
var result = await SupabaseService.Client.From<Budget>().Update(budget);
if (result.Model is null) return;
var item = Budgets.FirstOrDefault(x => x.Id == result.Model.Id);
if (item is null) return;
var index = Budgets.IndexOf(item);
if (index != -1) Budgets[index] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task DeleteBudget(Guid BudgetId)
{
try
{
await SupabaseService.Client.From<Budget>().Where(x => x.Id == BudgetId).Delete();
var item = Budgets.FirstOrDefault(x => x.Id == BudgetId);
if (item is null) return;
Budgets.Remove(item);
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public Account? PrimaryAccount => Accounts.FirstOrDefault(a => a.IsPrimary);
/// <summary>
/// Clears is_primary on the current primary account (if different from <paramref name="newPrimaryId"/>).
/// The caller must still save the new primary account via InsertAccount or UpdateAccount.
/// </summary>
public async Task SetPrimaryAccountAsync(Guid newPrimaryId)
{
try
{
var old = Accounts.FirstOrDefault(a => a.IsPrimary && a.Id != newPrimaryId);
if (old is null) return;
old.IsPrimary = false;
var result = await SupabaseService.Client.From<Account>().Update(old);
if (result.Model is null) return;
var idx = Accounts.IndexOf(old);
if (idx != -1) Accounts[idx] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
public async Task RefreshLiveRatesAndEnrich()
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
await CurrencyService.RefreshLiveRatesAsync(primaryCurrency, Accounts.Select(a => a.Currency));
LinkTransactionAccounts();
WeakReferenceMessenger.Default.Send(new RatesRefreshed());
}
public void LinkTransactionCategories()
{
foreach (var transaction in Transactions)
{
transaction.Category = Categories.FirstOrDefault(x => x.Id == transaction.CategoryId);
}
}
public Transaction LinkTransactionCategories(Transaction transaction)
{
transaction.Category = Categories.FirstOrDefault(x => x.Id == transaction.CategoryId);
return transaction;
}
public void LinkTransactionAccounts()
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
var primarySymbol = CurrencyService.GetSymbol(primaryCurrency);
foreach (var tx in Transactions)
{
EnrichTransactionAccount(tx, primaryCurrency, primarySymbol);
}
}
public Transaction LinkTransactionAccounts(Transaction tx)
{
var primaryCurrency = PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD";
var primarySymbol = CurrencyService.GetSymbol(primaryCurrency);
EnrichTransactionAccount(tx, primaryCurrency, primarySymbol);
return tx;
}
private void EnrichTransactionAccount(Transaction tx, string primaryCurrency, string primarySymbol)
{
var account = Accounts.FirstOrDefault(a => a.Id == tx.AccountId);
var accountCurrency = account?.Currency ?? primaryCurrency;
tx.AccountCurrency = accountCurrency;
tx.IsMultiCurrency = !accountCurrency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase);
tx.PrimaryAmountFormatted = $"{primarySymbol}{tx.ConvertedAmount:N2}";
tx.OriginalAmountFormatted = tx.IsMultiCurrency
? $"{CurrencyService.GetSymbol(accountCurrency)}{tx.Amount:N2}"
: string.Empty;
}
public async Task UpdateSavingsGoal(decimal? goal)
{
var profile = Profile;
profile.SavingsGoal = goal;
var result = await SupabaseService.Client.From<Profile>().Update(profile);
if (result.Models.Count < 1) return;
Profile = result.Models[0];
}
public async Task UpdateProfile(Profile profile)
{
var result = await SupabaseService.Client.From<Profile>().Update(profile);
if (result.Models.Count > 0) Profile = result.Models[0];
}
public async Task UpdateProfileAvatar(string? avatarUrl)
{
var profile = Profile;
profile.AvatarUrl = avatarUrl;
var result = await SupabaseService.Client
.From<Profile>()
.Update(profile);
Profile = result.Models[0];
}
/// <summary>Upload a local file as the current user's avatar. Returns the public URL.</summary>
public async Task<string> UploadAvatarAsync(string localFilePath)
{
var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
var ext = Path.GetExtension(localFilePath).ToLowerInvariant();
var storagePath = $"{userId}/avatar{ext}";
var bytes = await File.ReadAllBytesAsync(localFilePath);
var mimeType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".webp" => "image/webp",
_ => "application/octet-stream"
};
var bucket = SupabaseService.Client.Storage.From(Bucket);
// Upsert: upload if not exists, replace if it does
await bucket.Upload(bytes, storagePath, new FileOptions
{
ContentType = mimeType,
Upsert = true
});
var stream = new MemoryStream(bytes);
// Append cache-buster so Avalonia Image re-fetches the new file
return $"{PublicBaseUrl}/{storagePath}?t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
}
/// <summary>Delete the current user's avatar from storage.</summary>
public async Task DeleteAvatarAsync()
{
var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
// Try both extensions since we don't track which was uploaded
var bucket = SupabaseService.Client.Storage.From(Bucket);
foreach (var ext in new[] { "jpg", "jpeg", "png", "webp" })
{
try
{
await bucket.Remove([$"{userId}/avatar.{ext}"]);
}
catch
{
/* file with that ext may not exist, ignore */
}
}
}
/// <summary>Build the public URL for a given avatar_url stored in the profile.</summary>
public string? BuildPublicUrl(string? avatarUrl)
{
if (string.IsNullOrWhiteSpace(avatarUrl)) return null;
// If already a full URL (from storage or external), return as-is
if (avatarUrl.StartsWith("http")) return avatarUrl;
return $"{PublicBaseUrl}/{avatarUrl}";
}
} }

View File

@@ -0,0 +1,3 @@
namespace Clario.Messages;
public class RatesRefreshed { }

View File

@@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Clario.ViewModels" xmlns:vm="clr-namespace:Clario.ViewModels"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia" xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:views="clr-namespace:Clario.Views"
xmlns:model="clr-namespace:Clario.Models" xmlns:model="clr-namespace:Clario.Models"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="800"
x:Class="Clario.MobileViews.BudgetViewMobile" x:Class="Clario.MobileViews.BudgetViewMobile"
@@ -255,12 +254,6 @@
BorderThickness="0" BorderThickness="0"
Padding="4" Padding="4"
VerticalAlignment="Center"> VerticalAlignment="Center">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight"
FlyoutPresenterTheme="{StaticResource TransparentFlyoutPresenter}">
<views:BudgetCardMenuView />
</Flyout>
</Button.Flyout>
<Svg Path="../Assets/Icons/ellipsis.svg" <Svg Path="../Assets/Icons/ellipsis.svg"
Width="15" Height="15" Width="15" Height="15"
Css="{DynamicResource SvgMuted}" /> Css="{DynamicResource SvgMuted}" />
@@ -434,7 +427,7 @@
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" /> <TextBlock Grid.Column="0" Text="Monthly goal" FontSize="12" Foreground="{DynamicResource TextMuted}" />
<TextBlock Grid.Column="1" Text="{Binding Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold" <TextBlock Grid.Column="1" Text="{Binding AppData.Profile.SavingsGoal, StringFormat='$0'}" FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimary}" /> Foreground="{DynamicResource TextPrimary}" />
</Grid> </Grid>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
@@ -447,7 +440,7 @@
<ProgressBar Classes="yellow" <ProgressBar Classes="yellow"
Value="{Binding TotalLeft}" Value="{Binding TotalLeft}"
Minimum="0" Minimum="0"
Maximum="{Binding Profile.SavingsGoal}" Maximum="{Binding AppData.Profile.SavingsGoal}"
Height="6" /> Height="6" />
<Border Background="{DynamicResource BadgeBgYellow}" <Border Background="{DynamicResource BadgeBgYellow}"

View File

@@ -193,10 +193,16 @@
<TextBlock Text="Income" <TextBlock Text="Income"
FontSize="11" FontSize="11"
Foreground="{DynamicResource TextMuted}" /> Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalIncome, StringFormat='$0.00'}" <TextBlock FontSize="13"
FontSize="13"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource AccentGreen}" /> Foreground="{DynamicResource AccentGreen}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimaryCurrencySymbol" />
<Binding Path="TotalIncome" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -212,10 +218,16 @@
<TextBlock Text="Expenses" <TextBlock Text="Expenses"
FontSize="11" FontSize="11"
Foreground="{DynamicResource TextMuted}" /> Foreground="{DynamicResource TextMuted}" />
<TextBlock Text="{Binding TotalExpenses, StringFormat='$0.00'}" <TextBlock FontSize="13"
FontSize="13"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource AccentRed}" /> Foreground="{DynamicResource AccentRed}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}{1:N2}">
<Binding Path="PrimaryCurrencySymbol" />
<Binding Path="TotalExpenses" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -238,6 +250,7 @@
<MultiBinding Converter="{StaticResource NetworthSumConverter}"> <MultiBinding Converter="{StaticResource NetworthSumConverter}">
<Binding Path="TotalIncome" /> <Binding Path="TotalIncome" />
<Binding Path="TotalExpenses" /> <Binding Path="TotalExpenses" />
<Binding Path="PrimaryCurrencySymbol" />
</MultiBinding> </MultiBinding>
</TextBlock.Text> </TextBlock.Text>
</TextBlock> </TextBlock>
@@ -313,13 +326,22 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- Amount --> <!-- Dual-currency amount display -->
<TextBlock Grid.Column="2" <StackPanel Grid.Column="2"
Text="{Binding Amount, StringFormat='$0.00'}" HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding PrimaryAmountFormatted}"
FontSize="14" FontSize="14"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" HorizontalAlignment="Right"
VerticalAlignment="Center" /> Foreground="{Binding Type, Converter={StaticResource AmountColorConverter}}" />
<TextBlock Text="{Binding OriginalAmountFormatted}"
FontSize="11"
HorizontalAlignment="Right"
IsVisible="{Binding IsMultiCurrency}"
Foreground="{DynamicResource TextMuted}" />
</StackPanel>
</Grid> </Grid>
</Border> </Border>

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Clario.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models; using Supabase.Postgrest.Models;
@@ -30,7 +31,7 @@ public class Account : BaseModel
[Column("is_archived")] public bool IsArchived { get; set; } [Column("is_archived")] public bool IsArchived { get; set; }
[Column("opened_at")] public DateOnly? OpenedAt { get; set; } [Column("opened_at")] public DateTime? OpenedAt { get; set; }
[Column("created_at")] public DateTime CreatedAt { get; set; } [Column("created_at")] public DateTime CreatedAt { get; set; }
@@ -38,6 +39,8 @@ public class Account : BaseModel
[Column("color")] public string Color { get; set; } = string.Empty; [Column("color")] public string Color { get; set; } = string.Empty;
[Column("is_primary")] public bool IsPrimary { get; set; }
[JsonIgnore] public int TransactionsCount { get; set; } [JsonIgnore] public int TransactionsCount { get; set; }
[JsonIgnore] public int IncomeTransactionsThisMonth { get; set; } [JsonIgnore] public int IncomeTransactionsThisMonth { get; set; }
[JsonIgnore] public int ExpenseTransactionsThisMonth { get; set; } [JsonIgnore] public int ExpenseTransactionsThisMonth { get; set; }
@@ -45,5 +48,14 @@ public class Account : BaseModel
[JsonIgnore] public decimal TotalExpenseThisMonth { get; set; } [JsonIgnore] public decimal TotalExpenseThisMonth { get; set; }
[JsonIgnore] public decimal MonthlyIncrease { get; set; } [JsonIgnore] public decimal MonthlyIncrease { get; set; }
[JsonIgnore] public List<Transaction>? RecentTransactions { get; set; } [JsonIgnore] public List<Transaction>? RecentTransactions { get; set; }
[JsonIgnore] public bool isCredit => Type == "Credit";
[JsonIgnore] public decimal CreditUtilizationPerc => (CurrentBalance < 0 ? CurrentBalance * -1 : 0) / (CreditLimit == 0 ? 1 : CreditLimit) ?? 1;
[JsonIgnore] public bool GroupHeader { get; set; } = false; [JsonIgnore] public bool GroupHeader { get; set; } = false;
[JsonIgnore] public string CurrencySymbol => CurrencyService.GetSymbol(Currency);
[JsonIgnore] public string CurrentBalanceFormatted => $"{CurrencySymbol}{CurrentBalance:N2}";
[JsonIgnore] public string TotalIncomeFormatted => $"{CurrencySymbol}{TotalIncomeThisMonth:N2}";
[JsonIgnore] public string TotalExpenseFormatted => $"{CurrencySymbol}{TotalExpenseThisMonth:N2}";
[JsonIgnore] public string MonthlyIncreaseFormatted =>
$"{(MonthlyIncrease >= 0 ? "+" : "-")}{CurrencySymbol}{Math.Abs(MonthlyIncrease):N2}";
} }

View File

@@ -29,22 +29,24 @@ public class Budget : BaseModel
[JsonIgnore] public Category? Category { get; set; } [JsonIgnore] public Category? Category { get; set; }
[JsonIgnore] public int TransactionsCount { get; set; } [JsonIgnore] public int TransactionsCount { get; set; }
[JsonIgnore] public decimal Spent { get; set; } // populated after joining with transactions [JsonIgnore] public decimal Spent { get; set; }
[JsonIgnore] public string PrimarySymbol { get; set; } = "$";
[JsonIgnore] public decimal Remaining => LimitAmount - Spent; [JsonIgnore] public decimal Remaining => LimitAmount - Spent;
[JsonIgnore] public double PercentageUsed => LimitAmount > 0 ? Math.Round((double)(Spent / LimitAmount), 2) : 0; [JsonIgnore] public double PercentageUsed => LimitAmount > 0 ? Math.Round((double)(Spent / LimitAmount), 2) : 0;
[JsonIgnore] public bool IsOverBudget => Spent > LimitAmount; [JsonIgnore] public bool IsOverBudget => Spent > LimitAmount;
[JsonIgnore] public bool IsWarning => !IsOverBudget && PercentageUsed * 100 >= AlertThreshold; [JsonIgnore] public bool IsWarning => !IsOverBudget && PercentageUsed * 100 >= AlertThreshold;
[JsonIgnore] public bool IsOnTrack => !IsOverBudget && PercentageUsed * 100 < AlertThreshold; [JsonIgnore] public bool IsOnTrack => PercentageUsed * 100 < AlertThreshold;
[JsonIgnore] public string SpentFormatted => $"${Spent:N0}"; [JsonIgnore] public string SpentFormatted => $"{PrimarySymbol}{Spent:N0}";
[JsonIgnore] public string AmountFormatted => $"of ${LimitAmount:N0}"; [JsonIgnore] public string LimitFormatted => $"{PrimarySymbol}{LimitAmount:N0}";
[JsonIgnore] public string AmountFormatted => $"of {PrimarySymbol}{LimitAmount:N0}";
[JsonIgnore] public string PercentageFormatted => $"{PercentageUsed:P0} used"; [JsonIgnore] public string PercentageFormatted => $"{PercentageUsed:P0} used";
[JsonIgnore] [JsonIgnore]
public string RemainingFormatted => IsOverBudget public string RemainingFormatted => IsOverBudget
? $"${Math.Abs(Remaining):N0} over" ? $"{PrimarySymbol}{Math.Abs(Remaining):N0} over"
: $"${Remaining:N0} left"; : $"{PrimarySymbol}{Remaining:N0} left";
[JsonIgnore] public bool GroupHeader { get; set; } = false; [JsonIgnore] public bool GroupHeader { get; set; } = false;
} }

View File

@@ -1,4 +1,6 @@
using System; using System;
using Avalonia.Media.Imaging;
using Newtonsoft.Json;
using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models; using Supabase.Postgrest.Models;
@@ -10,6 +12,8 @@ public class Profile : BaseModel
[PrimaryKey("id", false)] public Guid Id { get; set; } [PrimaryKey("id", false)] public Guid Id { get; set; }
[Column("display_name")] public string DisplayName { get; set; } [Column("display_name")] public string DisplayName { get; set; }
[Column("avatar_url")] public string? AvatarUrl { get; set; } [Column("avatar_url")] public string? AvatarUrl { get; set; }
[JsonIgnore] public Bitmap? Avatar { get; set; }
[JsonIgnore] public bool HasAvatar => !string.IsNullOrWhiteSpace(AvatarUrl);
[Column("currency")] public string Currency { get; set; } [Column("currency")] public string Currency { get; set; }
[Column("theme")] public string Theme { get; set; } [Column("theme")] public string Theme { get; set; }
[Column("language")] public string Language { get; set; } [Column("language")] public string Language { get; set; }

View File

@@ -0,0 +1,9 @@
namespace Clario.Models;
public class TestDefaults
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Clario.Data; using Clario.Data;
using Clario.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models; using Supabase.Postgrest.Models;
@@ -16,19 +17,7 @@ public class Transaction : BaseModel
[Column("account_id")] public Guid AccountId { get; set; } [Column("account_id")] public Guid AccountId { get; set; }
private Guid? _categoryId; [Column("category_id")] public Guid? CategoryId { get; set; }
[Column("category_id")]
public Guid? CategoryId
{
get => _categoryId;
set
{
_categoryId = value;
Category = DataRepo.General.FetchCategories().Result.FirstOrDefault(x => x.Id == value);
}
}
[JsonIgnore] public Category? Category { get; set; } [JsonIgnore] public Category? Category { get; set; }
@@ -44,5 +33,20 @@ public class Transaction : BaseModel
[Column("created_at")] public DateTime CreatedAt { get; set; } [Column("created_at")] public DateTime CreatedAt { get; set; }
[Column("exchange_rate")] public decimal? ExchangeRate { 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 decimal ConvertedAmount =>
!string.IsNullOrEmpty(AccountCurrency) && CurrencyService.LiveRates.TryGetValue(AccountCurrency, out var liveRate)
? Amount * liveRate
: (ExchangeRate.HasValue ? Amount * ExchangeRate.Value : Amount);
[JsonIgnore] public bool IsMultiCurrency { get; set; }
[JsonIgnore] public string PrimaryAmountSignFormatted =>
Type == "expense" ? $"-{PrimaryAmountFormatted}" : $"+{PrimaryAmountFormatted}";
[JsonIgnore] public bool GroupHeader { get; set; } = false; [JsonIgnore] public bool GroupHeader { get; set; } = false;
} }

View File

@@ -0,0 +1,78 @@
// using System;
// using System.IO;
// using System.Threading.Tasks;
// using Avalonia.Media.Imaging;
// using Clario.Data;
// using Supabase.Storage;
// using FileOptions = Supabase.Storage.FileOptions;
//
// namespace Clario.Services;
//
// public class AvatarService
// {
// public static AvatarService Instance = new();
//
// private const string Bucket = "avatars";
// private const string ProjectRef = "xzxstbllaivumhtpctmo";
// private const string PublicBaseUrl = $"https://{ProjectRef}.supabase.co/storage/v1/object/public/{Bucket}";
//
// /// <summary>Upload a local file as the current user's avatar. Returns the public URL.</summary>
// public async Task<string> UploadAvatarAsync(string localFilePath)
// {
// var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
// var ext = Path.GetExtension(localFilePath).ToLowerInvariant();
// var storagePath = $"{userId}/avatar{ext}";
//
// var bytes = await File.ReadAllBytesAsync(localFilePath);
// var mimeType = ext switch
// {
// ".jpg" or ".jpeg" => "image/jpeg",
// ".png" => "image/png",
// ".webp" => "image/webp",
// _ => "application/octet-stream"
// };
//
// var bucket = SupabaseService.Client.Storage.From(Bucket);
//
// // Upsert: upload if not exists, replace if it does
// await bucket.Upload(bytes, storagePath, new FileOptions
// {
// ContentType = mimeType,
// Upsert = true
// });
//
// var stream = new MemoryStream(bytes);
// DataRepo.General.Profile!.Avatar = new Bitmap(stream);
// // Append cache-buster so Avalonia Image re-fetches the new file
// return $"{PublicBaseUrl}/{storagePath}?t={DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
// }
//
// /// <summary>Delete the current user's avatar from storage.</summary>
// public async Task DeleteAvatarAsync()
// {
// var userId = SupabaseService.Client.Auth.CurrentUser!.Id;
//
// // Try both extensions since we don't track which was uploaded
// var bucket = SupabaseService.Client.Storage.From(Bucket);
// foreach (var ext in new[] { "jpg", "jpeg", "png", "webp" })
// {
// try
// {
// await bucket.Remove([$"{userId}/avatar.{ext}"]);
// }
// catch
// {
// /* file with that ext may not exist, ignore */
// }
// }
// }
//
// /// <summary>Build the public URL for a given avatar_url stored in the profile.</summary>
// public static string? BuildPublicUrl(string? avatarUrl)
// {
// if (string.IsNullOrWhiteSpace(avatarUrl)) return null;
// // If already a full URL (from storage or external), return as-is
// if (avatarUrl.StartsWith("http")) return avatarUrl;
// return $"{PublicBaseUrl}/{avatarUrl}";
// }
// }

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace Clario.Services;
public static class CurrencyService
{
private static readonly HttpClient _http = new();
public static readonly IReadOnlyDictionary<string, string> CurrencyNames =
new Dictionary<string, string>
{
// Major
{ "USD", "US Dollar" },
{ "EUR", "Euro" },
{ "GBP", "British Pound" },
{ "JPY", "Japanese Yen" },
{ "CHF", "Swiss Franc" },
{ "CAD", "Canadian Dollar" },
{ "AUD", "Australian Dollar" },
{ "NZD", "New Zealand Dollar" },
// Asia-Pacific
{ "CNY", "Chinese Yuan" },
{ "HKD", "Hong Kong Dollar" },
{ "SGD", "Singapore Dollar" },
{ "KRW", "South Korean Won" },
{ "TWD", "Taiwan Dollar" },
{ "INR", "Indian Rupee" },
{ "PKR", "Pakistani Rupee" },
{ "BDT", "Bangladeshi Taka" },
{ "LKR", "Sri Lankan Rupee" },
{ "NPR", "Nepalese Rupee" },
{ "MMK", "Myanmar Kyat" },
{ "THB", "Thai Baht" },
{ "MYR", "Malaysian Ringgit" },
{ "IDR", "Indonesian Rupiah" },
{ "PHP", "Philippine Peso" },
{ "VND", "Vietnamese Dong" },
{ "KHR", "Cambodian Riel" },
{ "LAK", "Lao Kip" },
{ "MNT", "Mongolian Tögrög" },
{ "AFN", "Afghan Afghani" },
{ "BND", "Brunei Dollar" },
{ "MOP", "Macanese Pataca" },
// Middle East
{ "AED", "UAE Dirham" },
{ "SAR", "Saudi Riyal" },
{ "QAR", "Qatari Riyal" },
{ "KWD", "Kuwaiti Dinar" },
{ "BHD", "Bahraini Dinar" },
{ "OMR", "Omani Rial" },
{ "JOD", "Jordanian Dinar" },
{ "ILS", "Israeli Shekel" },
{ "IQD", "Iraqi Dinar" },
{ "YER", "Yemeni Rial" },
{ "LBP", "Lebanese Pound" },
// Africa
{ "EGP", "Egyptian Pound" },
{ "MAD", "Moroccan Dirham" },
{ "TND", "Tunisian Dinar" },
{ "DZD", "Algerian Dinar" },
{ "LYD", "Libyan Dinar" },
{ "NGN", "Nigerian Naira" },
{ "GHS", "Ghanaian Cedi" },
{ "KES", "Kenyan Shilling" },
{ "UGX", "Ugandan Shilling" },
{ "TZS", "Tanzanian Shilling" },
{ "ETB", "Ethiopian Birr" },
{ "ZAR", "South African Rand" },
{ "ZMW", "Zambian Kwacha" },
{ "BWP", "Botswana Pula" },
{ "MZN", "Mozambican Metical" },
{ "AOA", "Angolan Kwanza" },
{ "XOF", "West African CFA Franc" },
{ "XAF", "Central African CFA Franc" },
{ "MUR", "Mauritian Rupee" },
{ "RWF", "Rwandan Franc" },
{ "SDG", "Sudanese Pound" },
{ "MGA", "Malagasy Ariary" },
// Europe (non-EUR)
{ "SEK", "Swedish Krona" },
{ "NOK", "Norwegian Krone" },
{ "DKK", "Danish Krone" },
{ "ISK", "Icelandic Króna" },
{ "PLN", "Polish Złoty" },
{ "CZK", "Czech Koruna" },
{ "HUF", "Hungarian Forint" },
{ "RON", "Romanian Leu" },
{ "BGN", "Bulgarian Lev" },
{ "HRK", "Croatian Kuna" },
{ "RSD", "Serbian Dinar" },
{ "ALL", "Albanian Lek" },
{ "MKD", "Macedonian Denar" },
{ "BAM", "Bosnian Mark" },
{ "MDL", "Moldovan Leu" },
{ "UAH", "Ukrainian Hryvnia" },
{ "BYN", "Belarusian Ruble" },
{ "RUB", "Russian Ruble" },
{ "TRY", "Turkish Lira" },
// Caucasus & Central Asia
{ "GEL", "Georgian Lari" },
{ "AMD", "Armenian Dram" },
{ "AZN", "Azerbaijani Manat" },
{ "KZT", "Kazakhstani Tenge" },
{ "UZS", "Uzbekistani Som" },
{ "TJS", "Tajikistani Somoni" },
{ "TMT", "Turkmenistani Manat" },
{ "KGS", "Kyrgyzstani Som" },
// Americas
{ "MXN", "Mexican Peso" },
{ "BRL", "Brazilian Real" },
{ "ARS", "Argentine Peso" },
{ "CLP", "Chilean Peso" },
{ "COP", "Colombian Peso" },
{ "PEN", "Peruvian Sol" },
{ "BOB", "Bolivian Boliviano" },
{ "PYG", "Paraguayan Guaraní" },
{ "UYU", "Uruguayan Peso" },
{ "VES", "Venezuelan Bolívar" },
{ "GTQ", "Guatemalan Quetzal" },
{ "HNL", "Honduran Lempira" },
{ "NIO", "Nicaraguan Córdoba" },
{ "CRC", "Costa Rican Colón" },
{ "PAB", "Panamanian Balboa" },
{ "DOP", "Dominican Peso" },
{ "JMD", "Jamaican Dollar" },
{ "TTD", "Trinidad & Tobago Dollar" },
{ "BSD", "Bahamian Dollar" },
{ "CUP", "Cuban Peso" },
{ "HTG", "Haitian Gourde" },
{ "XCD", "Eastern Caribbean Dollar" },
{ "BBD", "Barbadian Dollar" },
{ "GYD", "Guyanese Dollar" },
{ "SRD", "Surinamese Dollar" },
};
public static IReadOnlyList<string> AvailableCurrencies { get; } =
new List<string>(CurrencyNames.Keys);
/// <summary>
/// Maps each account currency → rate to convert 1 unit to the current primary currency.
/// Populated on startup and refreshed whenever the primary currency changes.
/// </summary>
public static Dictionary<string, decimal> LiveRates { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Fetches fresh exchange rates for every currency in <paramref name="accountCurrencies"/>
/// relative to <paramref name="primaryCurrency"/> and stores them in <see cref="LiveRates"/>.
/// </summary>
public static async Task RefreshLiveRatesAsync(string primaryCurrency, IEnumerable<string> accountCurrencies)
{
var currencies = accountCurrencies.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var tasks = currencies.Select(async currency =>
{
if (currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
{
LiveRates[currency] = 1m;
return;
}
var rate = await GetExchangeRateAsync(currency, primaryCurrency);
if (rate.HasValue) LiveRates[currency] = rate.Value;
});
await Task.WhenAll(tasks);
}
/// <summary>
/// Fetches the live exchange rate from <paramref name="from"/> to <paramref name="to"/>
/// using the Frankfurter API. Returns null on failure.
/// </summary>
public static async Task<decimal?> GetExchangeRateAsync(string from, string to)
{
try
{
if (from.Equals(to, StringComparison.OrdinalIgnoreCase)) return 1m;
var url = $"https://api.frankfurter.dev/v2/rates?base={from.ToUpper()}&quotes={to.ToUpper()}";
var json = await _http.GetStringAsync(url);
var arr = JArray.Parse(json);
var rate = arr[0]?["rate"]?.Value<decimal>();
return Math.Round(rate ?? 1, 2);
}
catch
{
return null;
}
}
/// <summary>Returns the display symbol for a given ISO currency code.</summary>
public static string GetSymbol(string? code) => code?.ToUpper() switch
{
"USD" => "$",
"CAD" => "CA$",
"AUD" => "A$",
"NZD" => "NZ$",
"HKD" => "HK$",
"SGD" => "S$",
"BSD" => "B$",
"BND" => "B$",
"BBD" => "Bds$",
"EUR" => "€",
"GBP" => "£",
"EGP" => "E£",
"LBP" => "L£",
"SYP" => "S£",
"JPY" => "¥",
"CNY" => "¥",
"CHF" => "Fr",
"SEK" => "kr",
"NOK" => "kr",
"DKK" => "kr",
"ISK" => "kr",
"INR" => "₹",
"NPR" => "₨",
"PKR" => "₨",
"LKR" => "₨",
"MUR" => "₨",
"SCR" => "₨",
"BRL" => "R$",
"RUB" => "₽",
"KRW" => "₩",
"TRY" => "₺",
"ILS" => "₪",
"UAH" => "₴",
"KZT" => "₸",
"MNT" => "₮",
"THB" => "฿",
"VND" => "₫",
"PHP" => "₱",
"IDR" => "Rp",
"MYR" => "RM",
"KWD" => "KD",
"BHD" => "BD",
"OMR" => "OMR",
"JOD" => "JD",
"SAR" => "SR",
"AED" => "AED",
"QAR" => "QR",
"IQD" => "IQD",
"YER" => "YR",
"IRR" => "﷼",
"HUF" => "Ft",
"CZK" => "Kč",
"PLN" => "zł",
"RON" => "lei",
"BGN" => "лв",
"HRK" => "kn",
"RSD" => "din",
"GEL" => "₾",
"AMD" => "֏",
"AZN" => "₼",
"AFN" => "؋",
"NGN" => "₦",
"GHS" => "₵",
"ZAR" => "R",
"KES" => "Ksh",
"UGX" => "USh",
"TZS" => "TSh",
"ETB" => "Br",
"MAD" => "MAD",
"DZD" => "DA",
"TND" => "DT",
"XOF" => "CFA",
"XAF" => "FCFA",
"MXN" => "MX$",
"ARS" => "AR$",
"CLP" => "CL$",
"COP" => "CO$",
"PEN" => "S/",
"BOB" => "Bs",
"PYG" => "₲",
"UYU" => "$U",
"VES" => "Bs.S",
"GTQ" => "Q",
"HNL" => "L",
"NIO" => "C$",
"CRC" => "₡",
"DOP" => "RD$",
"JMD" => "J$",
"TTD" => "TT$",
"HTG" => "G",
"GYD" => "G$",
_ => code ?? "?"
};
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Diagnostics;
namespace Clario.Services;
public static class DebugLogger
{
[Conditional("DEBUG")]
public static void Log(object message) => Console.WriteLine(message);
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
namespace Clario.Services;
public class FilePickerService
{
public static FilePickerService Instance { get; } = new();
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;
}
public async Task<IStorageFile?> PickImageAsync()
{
var topLevel = GetTopLevel();
if (topLevel is null) return null;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Select Avatar Image",
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
new("Images") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.webp"] }
}
});
return files.Count > 0 ? files[0] : null;
}
}

View File

@@ -9,17 +9,13 @@ public class FileSessionStorage : ISessionStorage
public void Save(string json) public void Save(string json)
{ {
// Console.WriteLine($"Saving session to {_path}");
Directory.CreateDirectory(Path.GetDirectoryName(_path)!); Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
File.WriteAllText(_path, json); File.WriteAllText(_path, json);
} }
public string? Load() public string? Load()
{ {
if (!File.Exists(_path)) if (!File.Exists(_path)) return null;
{
return null;
}
var json = File.ReadAllText(_path); var json = File.ReadAllText(_path);
return json; return json;

View File

@@ -36,7 +36,7 @@ public class SupabaseService
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Session restore failed: {ex.Message}"); DebugLogger.Log($"Session restore failed: {ex.Message}");
sessionStorage.Delete(); // session invalid, delete it sessionStorage.Delete(); // session invalid, delete it
} }
} }

View File

@@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Styling; using Avalonia.Styling;
using Clario.Theme;
namespace Clario.Services; namespace Clario.Services;
@@ -21,11 +22,14 @@ public class ThemeService
{ {
"dark" => ThemeVariant.Dark, "dark" => ThemeVariant.Dark,
"light" => ThemeVariant.Light, "light" => ThemeVariant.Light,
"latte" => CustomAppThemeVariants.CatppuccinLatte,
"macchiato" => CustomAppThemeVariants.CatppuccinMacchiato,
"mocha" => CustomAppThemeVariants.CatppuccinMocha,
_ => ThemeVariant.Default _ => ThemeVariant.Default
}; };
app.RequestedThemeVariant = themeVariant; app.RequestedThemeVariant = themeVariant;
} }
public static bool IsDarkTheme => Application.Current?.RequestedThemeVariant == ThemeVariant.Dark; public static bool IsDarkTheme => Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
public static bool IsLightTheme => Application.Current?.RequestedThemeVariant == ThemeVariant.Light; public static bool IsLightTheme => Application.Current?.ActualThemeVariant == ThemeVariant.Light;
} }

View File

@@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Clario.CustomControls"> xmlns:cc="clr-namespace:Clario.CustomControls"
xmlns:theme="clr-namespace:Clario.Theme">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<cc:DateRangePicker SelectionMode="SingleRange" <cc:DateRangePicker SelectionMode="SingleRange"
@@ -11,8 +12,9 @@
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<StyleInclude Source="Styles/ToggleSwitchStyles.axaml" /> <StyleInclude Source="Styles/ToggleSwitchStyles.axaml" />
<!-- <StyleInclude Source="Styles/CalenderItemStyles.axaml" /> --> <StyleInclude Source="Styles/ColorPickerStyles.axaml" />
<StyleInclude Source="Styles/CalendarStyles.axaml" /> <StyleInclude Source="Styles/CalendarStyles.axaml" />
<StyleInclude Source="Styles/SliderStyles.axaml" />
<StyleInclude Source="../CustomControls/DateRangePicker.axaml" /> <StyleInclude Source="../CustomControls/DateRangePicker.axaml" />
<Styles.Resources> <Styles.Resources>
<ResourceDictionary> <ResourceDictionary>
@@ -58,15 +60,21 @@
<!-- LAYOUT --> <!-- LAYOUT -->
<x:Double x:Key="SidebarWidth">220</x:Double> <x:Double x:Key="SidebarWidth">220</x:Double>
<!-- FLYOUT PRESENTER THEME --> <!-- Shared Logo Assets -->
<ControlTheme x:Key="TransparentFlyoutPresenter"
TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent" /> <!-- Icon only, transparent bg -->
<Setter Property="BorderBrush" Value="Transparent" /> <x:String x:Key="LogoIconPrimaryTransparentSvg">avares://Clario/Assets/Logo/logo-icon-primary-transparent.svg</x:String>
<Setter Property="BorderThickness" Value="0" /> <Bitmap x:Key="LogoIconPrimaryTransparent1x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-128.png</Bitmap>
<Setter Property="Padding" Value="0" /> <Bitmap x:Key="LogoIconPrimaryTransparent2x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-256.png</Bitmap>
<Setter Property="CornerRadius" Value="0" /> <Bitmap x:Key="LogoIconPrimaryTransparent4x">avares://Clario/Assets/Logo/logo-icon-primary-transparent-512.png</Bitmap>
</ControlTheme>
<!-- Combined (icon + text), transparent bg -->
<x:String x:Key="LogoCombinedPrimaryTransparentSvg">avares://Clario/Assets/Logo/logo-combined-primary-transparent.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryTransparent1x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryTransparent2x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryTransparent4x">avares://Clario/Assets/Logo/logo-combined-primary-transparent-768x192.png</Bitmap>
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark"> <ResourceDictionary x:Key="Dark">
@@ -95,6 +103,10 @@
<SolidColorBrush x:Key="AccentOrange" Color="#FF7E5E" /> <SolidColorBrush x:Key="AccentOrange" Color="#FF7E5E" />
<SolidColorBrush x:Key="AccentPink" Color="#FF5E9B" /> <SolidColorBrush x:Key="AccentPink" Color="#FF5E9B" />
<SolidColorBrush x:Key="DangerButtonBackground" Color="#2A0D0D" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#3A1515" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#3A1515" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#4A1C1C" />
<!-- SVG COLORS --> <!-- SVG COLORS -->
@@ -147,6 +159,26 @@
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#6B8AEF" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#6B8AEF" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#4A5A8A" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#4A5A8A" />
<!-- logos -->
<!-- Icon only, dark bg -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<!-- Icon only, neutral -->
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<!-- Combined (icon + text) -->
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<!-- BACKGROUNDS --> <!-- BACKGROUNDS -->
@@ -174,6 +206,11 @@
<SolidColorBrush x:Key="AccentOrange" Color="#E8622A" /> <SolidColorBrush x:Key="AccentOrange" Color="#E8622A" />
<SolidColorBrush x:Key="AccentPink" Color="#D4306A" /> <SolidColorBrush x:Key="AccentPink" Color="#D4306A" />
<SolidColorBrush x:Key="DangerButtonBackground" Color="#FFE8E8" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#FFCCCC" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#FFD5D5" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#FFBCBC" />
<!-- ICON BACKGROUNDS --> <!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#E8EEFF" /> <SolidColorBrush x:Key="IconBgBlue" Color="#E8EEFF" />
<SolidColorBrush x:Key="IconBgGreen" Color="#E0F5EC" /> <SolidColorBrush x:Key="IconBgGreen" Color="#E0F5EC" />
@@ -212,13 +249,334 @@
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#5580FF" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#2D5CE8" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" /> <SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#A0B4FF" />
<Bitmap x:Key="key">path</Bitmap>
<!-- 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>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-512.png</Bitmap>
<!-- Icon only, neutral -->
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-512.png</Bitmap>
<!-- Combined (icon + text) -->
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-768x192.png</Bitmap>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinLatte}">
<!-- BACKGROUNDS -->
<SolidColorBrush x:Key="BgBase" Color="#e6e9ef" /> <!-- Mantle — app background -->
<SolidColorBrush x:Key="BgSidebar" Color="#dce0e8" /> <!-- Crust — sidebar (darker than bg) -->
<SolidColorBrush x:Key="BgSurface" Color="#eff1f5" /> <!-- Base — cards (lightest, elevated) -->
<SolidColorBrush x:Key="BgHover" Color="#ccd0da" /> <!-- Surface 0 — hover -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#bcc0cc" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="BorderAccent" Color="#8839ef" /> <!-- Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#4c4f69" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#5c5f77" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#6c6f85" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#9ca0b0" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#1e66f5" />
<SolidColorBrush x:Key="AccentGreen" Color="#40a02b" />
<SolidColorBrush x:Key="AccentYellow" Color="#df8e1d" />
<SolidColorBrush x:Key="AccentRed" Color="#d20f39" />
<SolidColorBrush x:Key="AccentPurple" Color="#8839ef" />
<SolidColorBrush x:Key="AccentOrange" Color="#fe640b" />
<SolidColorBrush x:Key="AccentPink" Color="#ea76cb" />
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#fce0e5" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#f7b8c4" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#f9cdd6" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#f4a5b6" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#d0dcfd" />
<SolidColorBrush x:Key="IconBgGreen" Color="#d6eecf" />
<SolidColorBrush x:Key="IconBgOrange" Color="#fee9d4" />
<SolidColorBrush x:Key="IconBgRed" Color="#fce0e5" />
<SolidColorBrush x:Key="IconBgPurple" Color="#e8d5fc" />
<SolidColorBrush x:Key="IconBgPink" Color="#fbd5f3" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#d6eecf" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#fce0e5" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#fdf0d0" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#d0dcfd" />
<SolidColorBrush x:Key="DividerAlpha" Color="#bcc0cc" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #eff1f5; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #4c4f69; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #5c5f77; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6c6f85; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #9ca0b0; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #1e66f5; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #40a02b; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #df8e1d; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #d20f39; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #8839ef; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #fe640b; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #ea76cb; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #eff1f5; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #4c4f69; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #5c5f77; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6c6f85; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #9ca0b0; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #1e66f5; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #40a02b; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #df8e1d; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #d20f39; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #8839ef; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #fe640b; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #ea76cb; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#bcc0cc" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#acb0be" />
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#eff1f5" />
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#7c7f93" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#7151c8" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#5a3db5" />
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#c9aafc" />
<!-- LOGOS -->
<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>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-light-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-dark-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-light.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-light-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinMacchiato}">
<!-- BACKGROUNDS -->
<!-- JetBrains: panelBackground=Mantle, editorBackground=Base, popup=Surface0, hover=Surface1 -->
<SolidColorBrush x:Key="BgBase" Color="#24273a" /> <!-- Base -->
<SolidColorBrush x:Key="BgSidebar" Color="#1e2030" /> <!-- Mantle -->
<SolidColorBrush x:Key="BgSurface" Color="#363a4f" /> <!-- Surface 0 -->
<SolidColorBrush x:Key="BgHover" Color="#494d64" /> <!-- Surface 1 -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#5b6078" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="BorderAccent" Color="#c6a0f6" /> <!-- Mauve — JB focusColor=Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#cad3f5" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#b8c0e0" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#a5adcb" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#6e738d" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#8aadf4" /> <!-- Blue -->
<SolidColorBrush x:Key="AccentGreen" Color="#a6da95" /> <!-- Green -->
<SolidColorBrush x:Key="AccentYellow" Color="#eed49f" /> <!-- Yellow -->
<SolidColorBrush x:Key="AccentRed" Color="#ed8796" /> <!-- Red -->
<SolidColorBrush x:Key="AccentPurple" Color="#c6a0f6" /> <!-- Mauve -->
<SolidColorBrush x:Key="AccentOrange" Color="#f5a97f" /> <!-- Peach -->
<SolidColorBrush x:Key="AccentPink" Color="#f5bde6" /> <!-- Pink -->
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#3a1a22" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#5a2535" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#4a2030" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#6a2d3f" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#1f2c4a" />
<SolidColorBrush x:Key="IconBgGreen" Color="#1e3328" />
<SolidColorBrush x:Key="IconBgOrange" Color="#3a2615" />
<SolidColorBrush x:Key="IconBgRed" Color="#3a1a22" />
<SolidColorBrush x:Key="IconBgPurple" Color="#2a1f3d" />
<SolidColorBrush x:Key="IconBgPink" Color="#38163a" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#1e3328" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#3a1a22" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#35290f" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#1f2c4a" />
<SolidColorBrush x:Key="DividerAlpha" Color="#5b6078" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #24273a; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cad3f5; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #b8c0e0; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a5adcb; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6e738d; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #8aadf4; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6da95; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #eed49f; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #ed8796; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #c6a0f6; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5a97f; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5bde6; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #24273a; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cad3f5; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #b8c0e0; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a5adcb; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6e738d; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #8aadf4; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6da95; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #eed49f; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #ed8796; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #c6a0f6; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5a97f; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5bde6; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#494d64" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#5b6078" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#24273a" /> <!-- Base -->
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#939ab7" /> <!-- Overlay 2 -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#d4baff" /> <!-- Mauve brightened -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#a980e8" /> <!-- Mauve pressed -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#4a3a60" /><!-- Mauve muted -->
<!-- LOGOS -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static theme:CustomAppThemeVariants.CatppuccinMocha}">
<!-- BACKGROUNDS -->
<!-- JetBrains: panelBackground=Mantle, editorBackground=Base, popup=Surface0, hover=Surface1 -->
<SolidColorBrush x:Key="BgBase" Color="#1e1e2e" /> <!-- Base -->
<SolidColorBrush x:Key="BgSidebar" Color="#181825" /> <!-- Mantle -->
<SolidColorBrush x:Key="BgSurface" Color="#313244" /> <!-- Surface 0 -->
<SolidColorBrush x:Key="BgHover" Color="#45475a" /> <!-- Surface 1 -->
<!-- BORDERS -->
<SolidColorBrush x:Key="BorderSubtle" Color="#585b70" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="BorderAccent" Color="#cba6f7" /> <!-- Mauve — JB focusColor=Mauve -->
<!-- TEXT -->
<SolidColorBrush x:Key="TextPrimary" Color="#cdd6f4" /> <!-- Text -->
<SolidColorBrush x:Key="TextSecondary" Color="#bac2de" /> <!-- Subtext 1 -->
<SolidColorBrush x:Key="TextMuted" Color="#a6adc8" /> <!-- Subtext 0 -->
<SolidColorBrush x:Key="TextDisabled" Color="#6c7086" /> <!-- Overlay 0 -->
<!-- ACCENTS -->
<SolidColorBrush x:Key="AccentBlue" Color="#89b4fa" /> <!-- Blue -->
<SolidColorBrush x:Key="AccentGreen" Color="#a6e3a1" /> <!-- Green -->
<SolidColorBrush x:Key="AccentYellow" Color="#f9e2af" /> <!-- Yellow -->
<SolidColorBrush x:Key="AccentRed" Color="#f38ba8" /> <!-- Red -->
<SolidColorBrush x:Key="AccentPurple" Color="#cba6f7" /> <!-- Mauve -->
<SolidColorBrush x:Key="AccentOrange" Color="#fab387" /> <!-- Peach -->
<SolidColorBrush x:Key="AccentPink" Color="#f5c2e7" /> <!-- Pink -->
<!-- DANGER -->
<SolidColorBrush x:Key="DangerButtonBackground" Color="#3d1424" />
<SolidColorBrush x:Key="DangerButtonBorder" Color="#5e1e38" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#4d1a2e" />
<SolidColorBrush x:Key="DangerButtonBorderBrush" Color="#6e2442" />
<!-- ICON BACKGROUNDS -->
<SolidColorBrush x:Key="IconBgBlue" Color="#1a2847" />
<SolidColorBrush x:Key="IconBgGreen" Color="#1a3025" />
<SolidColorBrush x:Key="IconBgOrange" Color="#3d2410" />
<SolidColorBrush x:Key="IconBgRed" Color="#3d1424" />
<SolidColorBrush x:Key="IconBgPurple" Color="#291c3f" />
<SolidColorBrush x:Key="IconBgPink" Color="#3a1035" />
<!-- SEMANTIC OVERLAYS -->
<SolidColorBrush x:Key="BadgeBgGreen" Color="#1a3025" />
<SolidColorBrush x:Key="BadgeBgRed" Color="#3d1424" />
<SolidColorBrush x:Key="BadgeBgYellow" Color="#3a2c0a" />
<SolidColorBrush x:Key="BadgeBgBlue" Color="#1a2847" />
<SolidColorBrush x:Key="DividerAlpha" Color="#585b70" />
<!-- SVG COLORS -->
<x:String x:Key="SvgBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #1e1e2e; }</x:String>
<x:String x:Key="SvgPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cdd6f4; }</x:String>
<x:String x:Key="SvgSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #bac2de; }</x:String>
<x:String x:Key="SvgMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6adc8; }</x:String>
<x:String x:Key="SvgDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #6c7086; }</x:String>
<x:String x:Key="SvgBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #89b4fa; }</x:String>
<x:String x:Key="SvgGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #a6e3a1; }</x:String>
<x:String x:Key="SvgYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f9e2af; }</x:String>
<x:String x:Key="SvgRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f38ba8; }</x:String>
<x:String x:Key="SvgPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #cba6f7; }</x:String>
<x:String x:Key="SvgOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #fab387; }</x:String>
<x:String x:Key="SvgPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { stroke: #f5c2e7; }</x:String>
<x:String x:Key="SvgFillBase">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #1e1e2e; }</x:String>
<x:String x:Key="SvgFillPrimary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cdd6f4; }</x:String>
<x:String x:Key="SvgFillSecondary">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #bac2de; }</x:String>
<x:String x:Key="SvgFillMuted">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6adc8; }</x:String>
<x:String x:Key="SvgFillDisabled">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #6c7086; }</x:String>
<x:String x:Key="SvgFillBlue">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #89b4fa; }</x:String>
<x:String x:Key="SvgFillGreen">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #a6e3a1; }</x:String>
<x:String x:Key="SvgFillYellow">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f9e2af; }</x:String>
<x:String x:Key="SvgFillRed">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f38ba8; }</x:String>
<x:String x:Key="SvgFillPurple">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #cba6f7; }</x:String>
<x:String x:Key="SvgFillOrange">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #fab387; }</x:String>
<x:String x:Key="SvgFillPink">path, circle, rect, ellipse, line, polyline, polygon, text, use { fill: #f5c2e7; }</x:String>
<!-- TOGGLE SWITCH -->
<SolidColorBrush x:Key="ToggleSwitchTrackOff" Color="#45475a" /> <!-- Surface 1 -->
<SolidColorBrush x:Key="ToggleSwitchTrackBorderOff" Color="#585b70" /> <!-- Surface 2 -->
<SolidColorBrush x:Key="ToggleSwitchKnobOn" Color="#1e1e2e" /> <!-- Base -->
<SolidColorBrush x:Key="ToggleSwitchKnobOff" Color="#9399b2" /> <!-- Overlay 2 -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnHover" Color="#d9baff" /> <!-- Mauve brightened -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnPressed" Color="#ae87e8" /> <!-- Mauve pressed -->
<SolidColorBrush x:Key="ToggleSwitchTrackOnDisabled" Color="#3d2a5e" /><!-- Mauve muted -->
<!-- LOGOS -->
<x:String x:Key="LogoIconPrimaryBgSvg">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoIconPrimaryBg1x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-128.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg2x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-256.png</Bitmap>
<Bitmap x:Key="LogoIconPrimaryBg4x">avares://Clario/Assets/Logo/logo-icon-primary-bg-dark-512.png</Bitmap>
<x:String x:Key="LogoIconNeutralTransparentSvg">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent.svg</x:String>
<Bitmap x:Key="LogoIconNeutralTransparent1x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-128.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent2x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-256.png</Bitmap>
<Bitmap x:Key="LogoIconNeutralTransparent4x">avares://Clario/Assets/Logo/logo-icon-neutral-light-transparent-512.png</Bitmap>
<x:String x:Key="LogoCombinedPrimaryBgSvg">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark.svg</x:String>
<Bitmap x:Key="LogoCombinedPrimaryBg1x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-192x64.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg2x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-384x128.png</Bitmap>
<Bitmap x:Key="LogoCombinedPrimaryBg4x">avares://Clario/Assets/Logo/logo-combined-primary-bg-dark-768x192.png</Bitmap>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
<ControlTheme x:Key="TransparentFlyoutPresenter"
TargetType="FlyoutPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
</ControlTheme>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>
<!-- WINDOW / SHELL --> <!-- WINDOW / SHELL -->
<Style Selector="Window, FlyoutPresenter, ToolTip"> <Style Selector="Window, FlyoutPresenter, ToolTip">
@@ -593,6 +951,30 @@
<Setter Property="FocusAdorner" Value="{x:Null}" /> <Setter Property="FocusAdorner" Value="{x:Null}" />
</Style> </Style>
<!-- DANGER BUTTON -->
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource DangerButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource DangerButtonBorder}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource AccentRed}" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource DangerButtonBackgroundBrush}" />
<Setter Property="Opacity" Value="0.85" />
</Style>
<Style Selector="Button.danger:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource DangerButtonBorderBrush}" />
</Style>
<Style Selector="Button.danger:disabled /template/ ContentPresenter">
<Setter Property="Opacity" Value="0.4" />
</Style>
<!-- BASE BUTTON --> <!-- BASE BUTTON -->
@@ -1045,5 +1427,56 @@
<Setter Property="Opacity" Value="0.5" /> <Setter Property="Opacity" Value="0.5" />
</Style> </Style>
<!-- ── Budget Card — On Track ─────────────────────── -->
<Style Selector="Border.budget-card">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Padding" Value="20" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── Budget Card — Warning ──────────────────────── -->
<Style Selector="Border.budget-card-warning">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentYellow}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Padding" Value="20" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── Budget Card — Over Budget ─────────────────── -->
<Style Selector="Border.budget-card-over">
<Setter Property="Background" Value="{DynamicResource BgSurface}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentRed}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Padding" Value="20" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<!-- ── 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 ───────────────────────────── -->
<Style Selector="Border.badge-warning">
<Setter Property="Background" Value="{DynamicResource BadgeBgYellow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentYellow}" />
<Setter Property="CornerRadius" Value="20" />
<Setter Property="Padding" Value="6,2" />
</Style>
<!-- ── Badge — Over ──────────────────────────────── -->
<Style Selector="Border.badge-over">
<Setter Property="Background" Value="{DynamicResource BadgeBgRed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentRed}" />
<Setter Property="CornerRadius" Value="20" />
<Setter Property="Padding" Value="6,2" />
</Style>
</Styles> </Styles>

View File

@@ -0,0 +1,10 @@
using Avalonia.Styling;
namespace Clario.Theme;
public static class CustomAppThemeVariants
{
public static readonly ThemeVariant CatppuccinLatte = new("CatppuccinLatte", ThemeVariant.Light);
public static readonly ThemeVariant CatppuccinMacchiato = new("CatppuccinMacchiato", ThemeVariant.Dark);
public static readonly ThemeVariant CatppuccinMocha = new("CatppuccinMocha", ThemeVariant.Dark);
}

View File

@@ -0,0 +1,210 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:primitives="using:Avalonia.Controls.Primitives">
<Design.PreviewWith>
<Border Padding="20" Background="#0D0F14">
<ColorPicker Width="200" Height="36"/>
</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"/>
<!-- Tab strip border -->
<SolidColorBrush x:Key="ColorViewTabBorderBrush" Color="#1E2330"/>
<!-- Content area background (below tab strip) -->
<SolidColorBrush x:Key="ColorViewContentBackgroundBrush" Color="#13161E"/>
<!-- Content area border (the top-border line between tabs and content) -->
<SolidColorBrush x:Key="ColorViewContentBorderBrush" Color="#1E2330"/>
<!-- Fluent text control resources used by component label borders + hex # border -->
<SolidColorBrush x:Key="TextControlBackgroundDisabled" Color="#1A1E2A"/>
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#1E2330"/>
<SolidColorBrush x:Key="TextControlForegroundDisabled" Color="#7A8090"/>
<!-- TextBox (hex input + component spinners) -->
<SolidColorBrush x:Key="TextControlBackground" Color="#13161E"/>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#1A1E2A"/>
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#13161E"/>
<SolidColorBrush x:Key="TextControlForeground" Color="#F0F2F8"/>
<SolidColorBrush x:Key="TextControlForegroundPointerOver" Color="#F0F2F8"/>
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#F0F2F8"/>
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#2A3050"/>
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#7B9CFF"/>
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#5A6070"/>
<!-- RadioButton (RGB/HSV toggle) -->
<SolidColorBrush x:Key="RadioButtonBackground" Color="#13161E"/>
<SolidColorBrush x:Key="RadioButtonBackgroundPointerOver" Color="#1A1E2A"/>
<SolidColorBrush x:Key="RadioButtonBackgroundPressed" Color="#1E2330"/>
<SolidColorBrush x:Key="RadioButtonBorderBrush" Color="#1E2330"/>
<SolidColorBrush x:Key="RadioButtonBorderBrushPointerOver" Color="#2A3050"/>
<SolidColorBrush x:Key="RadioButtonForeground" Color="#C8D0E8"/>
<SolidColorBrush x:Key="RadioButtonForegroundPointerOver" Color="#F0F2F8"/>
<SolidColorBrush x:Key="RadioButtonOuterEllipseStroke" Color="#7B9CFF"/>
<SolidColorBrush x:Key="RadioButtonOuterEllipseCheckedStroke" Color="#7B9CFF"/>
<SolidColorBrush x:Key="RadioButtonOuterEllipseCheckedFill" Color="#7B9CFF"/>
<SolidColorBrush x:Key="RadioButtonCheckGlyphFill" Color="#0D0F14"/>
<!-- ColorPreviewer -->
<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}"/>
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
</Style>
<!-- The DropDownButton inside ColorPicker -->
<Style Selector="ColorPicker /template/ DropDownButton">
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
<Setter Property="Foreground" Value="{DynamicResource TextSecondary}"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton:pointerover /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource BgHover}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton:pressed /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource BorderSubtle}"/>
</Style>
<!-- The chevron arrow inside DropDownButton -->
<Style Selector="ColorPicker /template/ DropDownButton /template/ PathIcon">
<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}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{DynamicResource RadiusControl}"/>
<!-- <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>
<!-- TabItem (spectrum / palette / components icons) -->
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="MinHeight" Value="48"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:selected">
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:pointerover">
<Setter Property="Foreground" Value="{DynamicResource TextPrimary}"/>
</Style>
<!-- PathIcon inside tab headers -->
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem PathIcon">
<Setter Property="Foreground" Value="{DynamicResource TextMuted}"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TabItem:selected PathIcon">
<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}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="0,4,4,0"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{DynamicResource AccentBlue}"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup TextBox:pointerover /template/ Border#PART_BorderElement">
<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}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="ColorPicker /template/ DropDownButton /template/ Popup NumericUpDown /template/ Border">
<Setter Property="Background" Value="{DynamicResource BgBase}"/>
<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"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
</Style>
<!-- Slider thumb -->
<Style Selector="primitives|ColorSlider /template/ Thumb">
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Background" Value="{DynamicResource TextPrimary}"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="BorderBrush" Value="{DynamicResource BgBase}"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
<Style Selector="primitives|ColorSlider /template/ Thumb:pointerover">
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
</Style>
<Style Selector="primitives|ColorSlider /template/ Thumb:pressed">
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
</Style>
<!-- Vertical slider (the third component slider beside the spectrum) -->
<Style Selector="primitives|ColorSlider[Orientation=Vertical]">
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="NaN"/>
</Style>
<!-- ═══════════════════════════════════════════════════════════
ColorPreviewer (accent color swatches at the bottom)
═══════════════════════════════════════════════════════════════ -->
<Style Selector="primitives|ColorPreviewer">
<Setter Property="Background" Value="{DynamicResource BgSurface}"/>
</Style>
<!-- Individual preview color swatch borders -->
<Style Selector="primitives|ColorPreviewer Border">
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtle}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
</Styles>

View File

@@ -0,0 +1,116 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="24" Background="#0D0F14" Width="320">
<StackPanel Spacing="24">
<Slider Minimum="0" Maximum="100" Value="30" />
<Slider Minimum="0" Maximum="100" Value="65" />
<Slider Minimum="0" Maximum="100" Value="80" IsEnabled="False" />
</StackPanel>
</Border>
</Design.PreviewWith>
<!-- Add to SliderStyles.axaml inside <Styles.Resources> -->
<Styles.Resources>
<ControlTheme x:Key="SliderRepeatButton" TargetType="RepeatButton">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
CornerRadius="2"
Height="4"
VerticalAlignment="Center" />
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type Slider}" TargetType="Slider">
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
<Setter Property="Foreground" Value="{DynamicResource AccentBlue}" />
<Style Selector="^:horizontal">
<Setter Property="MinHeight" Value="20" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="grid">
<Border x:Name="TrackBackground"
Height="4"
Margin="8,0"
Background="{DynamicResource BorderSubtle}"
CornerRadius="2"
VerticalAlignment="Center" />
<Track x:Name="PART_Track"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Horizontal">
<Track.DecreaseButton>
<RepeatButton x:Name="PART_DecreaseButton"
Background="{DynamicResource AccentBlue}"
Theme="{StaticResource SliderRepeatButton}" />
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton x:Name="PART_IncreaseButton"
Background="{DynamicResource BorderSubtle}"
Theme="{StaticResource SliderRepeatButton}" />
</Track.IncreaseButton>
<Thumb x:Name="thumb" Width="16" Height="16">
<Thumb.Template>
<ControlTemplate>
<Border Width="16" Height="16"
CornerRadius="8"
Background="{DynamicResource AccentBlue}"
BorderBrush="{DynamicResource BgSurface}"
BorderThickness="2"
BoxShadow="0 2 8 0 #40000000" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- Pointer over -->
<Style Selector="^:pointerover /template/ RepeatButton#PART_DecreaseButton">
<Setter Property="Background" Value="#8FAEFF" />
</Style>
<Style Selector="^:pointerover /template/ Thumb#thumb">
<Setter Property="Template">
<ControlTemplate>
<Border Width="16" Height="16" CornerRadius="8"
Background="#8FAEFF"
BorderBrush="{DynamicResource BgSurface}"
BorderThickness="2"
BoxShadow="0 2 12 0 #507B9CFF" />
</ControlTemplate>
</Setter>
</Style>
<!-- Pressed -->
<Style Selector="^:pressed /template/ RepeatButton#PART_DecreaseButton">
<Setter Property="Background" Value="#6B8AEF" />
</Style>
<Style Selector="^:pressed /template/ Thumb#thumb">
<Setter Property="Template">
<ControlTemplate>
<Border Width="14" Height="14" CornerRadius="7"
Background="#6B8AEF"
BorderBrush="{DynamicResource BgSurface}"
BorderThickness="2" />
</ControlTemplate>
</Setter>
</Style>
<!-- Disabled -->
<Style Selector="^:disabled /template/ Grid#grid">
<Setter Property="Opacity" Value="0.5" />
</Style>
</ControlTheme>
</Styles.Resources>
</Styles>

View File

@@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Linq;
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 AccountFormViewModel : ViewModelBase
{
public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
public string FormTitle => IsEditMode ? "Edit Account" : "New Account";
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Account";
// ── Fields ──────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _name = "";
[ObservableProperty] private string _selectedType = "Checking";
[ObservableProperty] private string? _institution;
[ObservableProperty] private string? _mask;
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _openingBalance = "0.00";
[ObservableProperty] private string _currency = "USD";
[ObservableProperty] private bool _isPrimary = false;
[ObservableProperty] private string _currencySearch = "";
[ObservableProperty] private int _currencyPage = 1;
private const int CurrencyPageSize = 30;
public List<string> FilteredCurrencies =>
string.IsNullOrWhiteSpace(CurrencySearch)
? CurrencyService.AvailableCurrencies.ToList()
: CurrencyService.AvailableCurrencies
.Where(c => c.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase)
|| (CurrencyService.CurrencyNames.TryGetValue(c, out var name)
&& name.Contains(CurrencySearch, StringComparison.OrdinalIgnoreCase)))
.ToList();
public List<string> VisibleCurrencies => FilteredCurrencies.Take(CurrencyPage * CurrencyPageSize).ToList();
public bool HasMoreCurrencies => FilteredCurrencies.Count > CurrencyPage * CurrencyPageSize;
partial void OnCurrencySearchChanged(string value)
{
_currencyPage = 1;
OnPropertyChanged(nameof(FilteredCurrencies));
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
partial void OnCurrencyPageChanged(int value)
{
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
[ObservableProperty] private string? _creditLimit;
[ObservableProperty] private List<DateTime>? _openedAtDates;
[ObservableProperty] private string _selectedIcon = "wallet";
[ObservableProperty] private string _selectedColor = "#3B82F6";
// ── 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 ──────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public bool IsValid =>
!string.IsNullOrWhiteSpace(Name) &&
decimal.TryParse(OpeningBalance, out _);
public bool IsCredit => SelectedType == "Credit";
// ── Callbacks ───────────────────────────────────────────
public Action? OnSaved;
public Action? OnCancelled;
// ── Edit mode: original account ─────────────────────────
private Guid? _editingId;
// ── Result account ──────────────────────────────────────
public Account? ResultAccount { get; set; }
// ── Commands ────────────────────────────────────────────
partial void OnSelectedTypeChanged(string value)
{
OnPropertyChanged(nameof(IsCredit));
}
[RelayCommand]
private void SetCurrency(string currency) => Currency = currency;
[RelayCommand]
private void LoadMoreCurrencies() => CurrencyPage++;
private void ResetCurrencyFilter()
{
_currencySearch = "";
_currencyPage = 1;
OnPropertyChanged(nameof(CurrencySearch));
OnPropertyChanged(nameof(VisibleCurrencies));
OnPropertyChanged(nameof(HasMoreCurrencies));
}
[RelayCommand]
private async Task Save()
{
ErrorMessage = null;
if (string.IsNullOrWhiteSpace(Name))
{
ErrorMessage = "Name is required.";
return;
}
if (!decimal.TryParse(OpeningBalance, out var balance))
{
ErrorMessage = "Please enter a valid opening balance.";
return;
}
decimal? creditLimitValue = null;
if (IsCredit && !string.IsNullOrWhiteSpace(CreditLimit))
{
if (!decimal.TryParse(CreditLimit, out var limit))
{
ErrorMessage = "Please enter a valid credit limit.";
return;
}
creditLimitValue = limit;
}
try
{
if (IsEditMode && _editingId.HasValue)
{
if (IsPrimary) await DataRepo.General.SetPrimaryAccountAsync(_editingId.Value);
var updated = new Account
{
Id = _editingId.Value,
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
Name = Name.Trim(),
Type = SelectedType,
Institution = Institution?.Trim(),
Mask = Mask?.Trim(),
Currency = Currency,
OpeningBalance = balance,
CreditLimit = creditLimitValue,
OpenedAt = OpenedAtDates?[0],
Icon = SelectedIcon,
Color = SelectedColor,
IsPrimary = IsPrimary,
};
await DataRepo.General.UpdateAccount(updated);
ResultAccount = updated;
}
else
{
var newId = Guid.NewGuid();
if (IsPrimary) await DataRepo.General.SetPrimaryAccountAsync(newId);
var account = new Account
{
Id = newId,
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
Name = Name.Trim(),
Type = SelectedType,
Institution = Institution?.Trim(),
Mask = Mask?.Trim(),
Currency = Currency,
OpeningBalance = balance,
CreditLimit = creditLimitValue,
OpenedAt = OpenedAtDates?[0],
Icon = SelectedIcon,
Color = SelectedColor,
IsPrimary = IsPrimary,
};
var result = await DataRepo.General.InsertAccount(account);
ResultAccount = result;
}
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
DebugLogger.Log(ex);
}
}
[RelayCommand]
private void Cancel()
{
OnCancelled?.Invoke();
}
// ── Public setup methods ─────────────────────────────────
/// <summary>Call this to open the form for adding a new account.</summary>
public void SetupForAdd()
{
IsEditMode = false;
_editingId = null;
Name = "";
SelectedType = "Checking";
Institution = null;
Mask = null;
OpeningBalance = "0.00";
ResetCurrencyFilter();
Currency = DataRepo.General.Profile?.Currency ?? "USD";
IsPrimary = false;
CreditLimit = null;
OpenedAtDates = null;
SelectedIcon = "wallet";
SelectedColor = "#3B82F6";
ErrorMessage = null;
ResultAccount = null;
}
/// <summary>Call this to open the form for editing an existing account.</summary>
public void SetupForEdit(Account account)
{
IsEditMode = true;
_editingId = account.Id;
CurrencySearch = "";
Name = account.Name;
SelectedType = AccountTypes.FirstOrDefault(t => t.Equals(account.Type, StringComparison.OrdinalIgnoreCase)) ?? account.Type;
Institution = account.Institution;
Mask = account.Mask;
OpeningBalance = account.OpeningBalance.ToString("0.00");
Currency = account.Currency;
IsPrimary = account.IsPrimary;
CreditLimit = account.CreditLimit?.ToString("0.00");
OpenedAtDates = account.OpenedAt.HasValue ? new List<DateTime> { account.OpenedAt.Value } : null;
SelectedIcon = account.Icon;
SelectedColor = account.Color;
ErrorMessage = null;
ResultAccount = account;
}
}

View File

@@ -2,9 +2,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Clario.Data; using Clario.Data;
using Clario.Models; using Clario.Models;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -13,18 +13,26 @@ namespace Clario.ViewModels;
public partial class AccountsViewModel : ViewModelBase public partial class AccountsViewModel : ViewModelBase
{ {
public required ViewModelBase parentViewModel; public required ViewModelBase parentViewModel;
public required List<Account> Accounts = new();
public required List<Transaction> Transactions = new(); public GeneralDataRepo AppData => DataRepo.General;
[ObservableProperty] private ObservableCollection<Account> _visibleAccounts = new(); [ObservableProperty] private ObservableCollection<Account> _visibleAccounts = new();
[ObservableProperty] private decimal _totalBalance = 0; [ObservableProperty] private decimal _totalBalance;
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private Account? _selectedAccount; [ObservableProperty] private Account? _selectedAccount;
[ObservableProperty] private bool _isAccountDeletionConfirmationVisible;
public bool CanDeleteAccount => VisibleAccounts.Count > 1;
[ObservableProperty] private bool _isDeleteDialogVisible;
[ObservableProperty] private DeleteAccountDialogViewModel _deleteDialog = new();
public AccountsViewModel() public AccountsViewModel()
{ {
AppData.Accounts.CollectionChanged += (_, _) => { Initialize(); };
Initialize();
} }
public async Task Initialize() public void Initialize()
{ {
FetchAndProcessAccountInfo(); FetchAndProcessAccountInfo();
GroupAccounts(); GroupAccounts();
@@ -33,9 +41,11 @@ public partial class AccountsViewModel : ViewModelBase
private void FetchAndProcessAccountInfo() private void FetchAndProcessAccountInfo()
{ {
foreach (var account in Accounts) TotalBalance = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
{ {
var accountTransactions = Transactions.Where(t => t.AccountId == account.Id).ToList(); var accountTransactions = AppData.Transactions.Where(t => t.AccountId == account.Id).ToList();
account.TransactionsCount = accountTransactions.Count; account.TransactionsCount = accountTransactions.Count;
account.CurrentBalance = account.OpeningBalance + accountTransactions.Sum(t => t.Type == "income" ? t.Amount : -t.Amount); 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.TotalIncomeThisMonth = accountTransactions.Where(t => t.Date.Month == DateTime.Now.Month && t.Type == "income").Sum(t => t.Amount);
@@ -46,26 +56,48 @@ public partial class AccountsViewModel : ViewModelBase
var lastMonthBalance = accountTransactions.Where(t => t.Date.Month == DateTime.Now.AddMonths(-1).Month && t.Type == "income") 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); .Sum(t => t.Type == "income" ? t.Amount : -t.Amount);
account.MonthlyIncrease = account.TotalIncomeThisMonth - account.TotalExpenseThisMonth - lastMonthBalance; account.MonthlyIncrease = account.TotalIncomeThisMonth - account.TotalExpenseThisMonth - lastMonthBalance;
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalBalance += account.CurrentBalance; TotalBalance += account.CurrentBalance;
else
TotalBalance += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
} }
} }
[RelayCommand]
private void CreateAccount()
{
((MainViewModel)parentViewModel).OpenAddAccount();
}
[RelayCommand]
private void EditAccount(Account account)
{
((MainViewModel)parentViewModel).OpenEditAccount(account);
}
private void GroupAccounts() private void GroupAccounts()
{ {
var accountTypes = new Dictionary<string, string>() var accountTypes = new List<string>()
{ {
{ "checking", "Cash & Checking" }, "Cash",
{ "savings", "Savings" }, "Checking",
{ "credit", "Credit" }, "Savings",
{ "investment", "Investments" } "Credit",
"Investment",
"Other"
}; };
VisibleAccounts.Clear();
foreach (var type in accountTypes) foreach (var type in accountTypes)
{ {
var accountsOfType = Accounts.Where(a => a.Type == type.Key).ToList(); var accountsOfType = AppData.Accounts
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(a => a.IsPrimary)
.ThenBy(a => a.CreatedAt)
.ToList();
if (accountsOfType.Any()) if (accountsOfType.Any())
{ {
var header = new Account { Name = type.Value.ToUpper(), GroupHeader = true }; var header = new Account { Name = type.ToUpper(), GroupHeader = true };
VisibleAccounts.Add(header); VisibleAccounts.Add(header);
foreach (var account in accountsOfType) foreach (var account in accountsOfType)
{ {
@@ -73,6 +105,21 @@ public partial class AccountsViewModel : ViewModelBase
} }
} }
} }
OnPropertyChanged(nameof(CanDeleteAccount));
}
[RelayCommand]
private void RequestDeleteAccount(Account account)
{
DeleteDialog.Setup(account, new ObservableCollection<Account>(AppData.Accounts));
DeleteDialog.OnDeleted = () =>
{
IsDeleteDialogVisible = false;
Initialize();
};
DeleteDialog.OnCancelled = () => IsDeleteDialogVisible = false;
IsDeleteDialogVisible = true;
} }
[RelayCommand] [RelayCommand]
@@ -86,6 +133,7 @@ public partial class AccountsViewModel : ViewModelBase
{ {
if (parentViewModel is MainViewModel mainViewModel) if (parentViewModel is MainViewModel mainViewModel)
{ {
if (SelectedAccount is null) return;
var vm = mainViewModel._transactionsViewModel; var vm = mainViewModel._transactionsViewModel;
vm.SelectedAccount = vm.Accounts.First(x => x.Id == SelectedAccount.Id); vm.SelectedAccount = vm.Accounts.First(x => x.Id == SelectedAccount.Id);
vm.LoadPageCommand.Execute(1); vm.LoadPageCommand.Execute(1);

View File

@@ -1,8 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Clario.Models;
using Clario.Services; using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -34,17 +38,25 @@ public partial class AuthViewModel : ViewModelBase
public AuthViewModel() public AuthViewModel()
{ {
Console.WriteLine("auth vm loaded"); DebugLogger.Log("auth vm loaded");
setDefaults(); setDefaults();
} }
[Conditional("DEBUG")]
private void setDefaults() private void setDefaults()
{ {
FirstName = "nouredeen"; if (!File.Exists("devsettings.json")) return;
LastName = "ghazal";
Email = "nouredeen.ghazal42@gmail.com"; var json = File.ReadAllText("devsettings.json");
Password = "Nour1Clario"; var config = JsonSerializer.Deserialize<Wrapper>(json);
ConfirmPassword = "Nour1Clario"; if (config?.TestDefaults is null) return;
FirstName = config.TestDefaults.FirstName;
LastName = config.TestDefaults.LastName;
Email = config.TestDefaults.Email;
Password = config.TestDefaults.Password;
ConfirmPassword = config.TestDefaults.Password;
ThemeService.SwitchToTheme("system"); ThemeService.SwitchToTheme("system");
} }
@@ -74,7 +86,7 @@ public partial class AuthViewModel : ViewModelBase
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
} }
} }
@@ -99,7 +111,6 @@ public partial class AuthViewModel : ViewModelBase
await SupabaseService.Client.Auth.SetSession(session.AccessToken, session.RefreshToken); await SupabaseService.Client.Auth.SetSession(session.AccessToken, session.RefreshToken);
var user = session.User; var user = session.User;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel(); desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
@@ -111,7 +122,7 @@ public partial class AuthViewModel : ViewModelBase
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
} }
} }
@@ -125,3 +136,8 @@ public partial class AuthViewModel : ViewModelBase
!string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_email) &&
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword; !string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
} }
class Wrapper
{
public TestDefaults TestDefaults { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace Clario.ViewModels;
public partial class BudgetCardMenuViewModel : ViewModelBase
{
}

View File

@@ -1,6 +1,211 @@
namespace Clario.ViewModels; using System;
using System.Collections.ObjectModel;
using System.Linq;
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 BudgetFormViewModel : ViewModelBase public partial class BudgetFormViewModel : ViewModelBase
{ {
public required ViewModelBase parentViewModel; public required ViewModelBase parentViewModel;
// ── Mode ────────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FormTitle), nameof(FormSubtitle), nameof(SaveButtonLabel))]
private bool _isEditMode = false;
public string FormTitle => IsEditMode ? "Edit Budget" : "New Budget";
public string FormSubtitle => IsEditMode ? "Update the details below" : "Fill in the details below";
public string SaveButtonLabel => IsEditMode ? "Save Changes" : "Save Budget";
// ── Fields ──────────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsMonthly), nameof(IsQuarterly), nameof(IsYearly), nameof(IsValid))]
private string _period = "monthly";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private string _limitAmount = "";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))]
private Category? _selectedCategory;
[ObservableProperty] private ObservableCollection<Category> _categories = new();
// AlertThreshold: 0100 int, stored as double for Slider binding
// Slider.Value is double; we round to int when saving
[ObservableProperty] [NotifyPropertyChangedFor(nameof(AlertThresholdLabel))]
private double _alertThreshold = 80;
public string AlertThresholdLabel => $"{(int)AlertThreshold}%";
[ObservableProperty] private bool _rollover = false;
// ── Validation ──────────────────────────────────────────
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
private string? _errorMessage;
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public bool IsMonthly => Period == "monthly";
public bool IsQuarterly => Period == "quarterly";
public bool IsYearly => Period == "yearly";
public bool IsValid =>
decimal.TryParse(LimitAmount, out var amt) && amt > 0 &&
SelectedCategory is not null;
// ── Callbacks ───────────────────────────────────────────
public Action? OnSaved;
public Action? OnCancelled;
public Action? OnDeleted;
[ObservableProperty] private bool _showDeleteConfirm = false;
// ── Edit mode: original budget ───────────────────────────
private Guid? _editingId;
// ── Result ──────────────────────────────────────────────
public Budget? ResultBudget { get; set; }
// ── Commands ────────────────────────────────────────────
[RelayCommand]
private void SetPeriod(string period)
{
Period = period;
}
[RelayCommand]
private async Task Save()
{
ErrorMessage = null;
if (!decimal.TryParse(LimitAmount, out var amt) || amt <= 0)
{
ErrorMessage = "Please enter a valid amount.";
return;
}
if (SelectedCategory is null)
{
ErrorMessage = "Please select a category.";
return;
}
try
{
if (IsEditMode && _editingId.HasValue)
{
var updated = new Budget
{
Id = _editingId.Value,
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id),
CategoryId = SelectedCategory.Id,
LimitAmount = amt,
Period = Period,
AlertThreshold = (int)Math.Round(AlertThreshold),
Rollover = Rollover,
Category = SelectedCategory,
};
await DataRepo.General.UpdateBudget(updated);
ResultBudget = updated;
}
else
{
var budget = new Budget
{
Id = Guid.NewGuid(),
UserId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!),
CategoryId = SelectedCategory.Id,
LimitAmount = amt,
Period = Period,
AlertThreshold = (int)Math.Round(AlertThreshold),
Rollover = Rollover,
Category = SelectedCategory,
};
await DataRepo.General.InsertBudget(budget);
ResultBudget = budget;
}
OnSaved?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Something went wrong. Please try again.";
DebugLogger.Log(ex);
}
}
[RelayCommand]
private async Task ConfirmDelete()
{
if (!IsEditMode || !_editingId.HasValue) return;
try
{
await DataRepo.General.DeleteBudget(_editingId.Value);
OnDeleted?.Invoke();
}
catch (Exception ex)
{
ErrorMessage = "Failed to delete budget.";
DebugLogger.Log(ex);
}
}
[RelayCommand]
private void RequestDelete()
{
ShowDeleteConfirm = true;
}
[RelayCommand]
private void CancelDelete()
{
ShowDeleteConfirm = false;
}
[RelayCommand]
private void Cancel()
{
OnCancelled?.Invoke();
}
// ── Public setup methods ─────────────────────────────────
/// <summary>Call this to open the form for adding a new budget.</summary>
public void SetupForAdd(ObservableCollection<Category> categories)
{
ShowDeleteConfirm = false;
IsEditMode = false;
_editingId = null;
Categories = categories;
LimitAmount = "";
Period = "monthly";
AlertThreshold = 80;
Rollover = false;
ErrorMessage = null;
SelectedCategory = categories.Count > 0 ? categories[0] : null;
ResultBudget = null;
}
/// <summary>Call this to open the form for editing an existing budget.</summary>
public void SetupForEdit(Budget budget, ObservableCollection<Category> categories)
{
ShowDeleteConfirm = false;
IsEditMode = true;
_editingId = budget.Id;
Categories = categories;
LimitAmount = budget.LimitAmount.ToString("0.00");
Period = budget.Period;
AlertThreshold = budget.AlertThreshold;
Rollover = budget.Rollover;
ErrorMessage = null;
SelectedCategory = categories.FirstOrDefault(c => c.Id == budget.CategoryId)
?? (categories.Count > 0 ? categories[0] : null);
ResultBudget = budget;
}
} }

View File

@@ -1,13 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Data;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Clario.Data; using Clario.Data;
using Clario.Messages;
using Clario.Models; using Clario.Models;
using Clario.Models.GeneralModels; using Clario.Services;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using LiveChartsCore; using LiveChartsCore;
@@ -20,17 +21,15 @@ namespace Clario.ViewModels;
public partial class BudgetViewModel : ViewModelBase public partial class BudgetViewModel : ViewModelBase
{ {
public required ViewModelBase parentViewModel; public required ViewModelBase parentViewModel;
[ObservableProperty] private Profile? _profile; public GeneralDataRepo AppData => DataRepo.General;
public required List<Budget> Budgets = new();
[ObservableProperty] private ObservableCollection<Budget> _visibleBudgets = new(); [ObservableProperty] private ObservableCollection<Budget> _visibleBudgets = new();
public required List<Category> Categories = new();
public required List<Transaction> Transactions = new();
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))] [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(NextPeriodCommand), nameof(PreviousPeriodCommand))]
private DateTime _currentPeriod = DateTime.Now.Date; private DateTime _currentPeriod = DateTime.Now.Date;
public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month; public bool CanGoToNextPeriod => CurrentPeriod.Month < DateTime.Now.Month;
public bool CanGoToPreviousPeriod => Transactions.Any() && CurrentPeriod.Month > Transactions.Min(x => x.Date.Month); public bool CanGoToPreviousPeriod => AppData.Transactions.Any() && CurrentPeriod.Month > AppData.Transactions.Min(x => x.Date.Month);
public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy"); public string CurrentPeriodFormatted => CurrentPeriod.ToString("MMMM yyyy");
[ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = []; [ObservableProperty] private ISeries[] _spendingBreakdownChartSeries = [];
@@ -41,11 +40,16 @@ public partial class BudgetViewModel : ViewModelBase
public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget."; public string SpentPercentageFormatted => (TotalSpent / TotalBudgeted).ToString("P0") + " of total budget.";
public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue); public decimal TotalLeft => Math.Clamp(Math.Round(TotalBudgeted - TotalSpent), 0, decimal.MaxValue);
public string TotalLeftFormatted => TotalLeft.ToString("C0") + " left"; private string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
public string TotalLeftFormatted => $"{PrimarySymbol}{TotalLeft:N0} left";
public string SavingsHint => TotalLeft >= (Profile != null ? Profile.SavingsGoal : 0) public bool HasSavingsGoal => AppData.Profile?.SavingsGoal is > 0;
? "You're on track!"
: $"Reduce your spending by ${Math.Round((Profile != null ? Profile.SavingsGoal ?? 0 : 0) - TotalLeft)} to hit your goal."; public bool IsSavingsGoalMet => HasSavingsGoal && TotalLeft >= (AppData.Profile!.SavingsGoal ?? 0);
public string SavingsHint => IsSavingsGoalMet
? "You're on track to meet your savings goal this month!"
: $"Reduce your spending by {PrimarySymbol}{((AppData.Profile?.SavingsGoal ?? 0) - TotalLeft):N0} to hit your goal.";
private int _onTrackCount; private int _onTrackCount;
private int _approachingCount; private int _approachingCount;
@@ -60,38 +64,65 @@ public partial class BudgetViewModel : ViewModelBase
private int PeriodDaysLeft => PeriodLength - PeriodDaysPassed; private int PeriodDaysLeft => PeriodLength - PeriodDaysPassed;
public string PeriodDaysLeftFormatted => PeriodDaysLeft == 1 ? PeriodDaysLeft + " day left" : PeriodDaysLeft + " days left"; public string PeriodDaysLeftFormatted => PeriodDaysLeft == 1 ? PeriodDaysLeft + " day left" : PeriodDaysLeft + " days left";
public string DailyBudgetLeftFormatted => ((TotalBudgeted - TotalSpent) / PeriodDaysLeft).ToString("C", new CultureInfo("en-US")); public string DailyBudgetLeftFormatted =>
$"{PrimarySymbol}{((TotalBudgeted - TotalSpent) / ((PeriodDaysLeft == 0) ? 1 : PeriodDaysLeft)):N2}";
public BudgetViewModel() public BudgetViewModel()
{ {
AppData.Budgets.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.Transactions.CollectionChanged += async (_, _) => { await Initialize(); };
AppData.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(AppData.Profile))
NotifyComputedPropertiesOnChanged();
};
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, async (_, _) => await Initialize());
_ = Initialize();
} }
public async Task Initialize() private async Task Initialize()
{ {
try try
{ {
await ProcessBudgets(); await ProcessBudgets();
ProcessChartData(); ProcessChartData();
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); DebugLogger.Log(e);
throw; throw;
} }
} }
[RelayCommand]
private void CreateBudget()
{
((MainViewModel)parentViewModel).OpenAddBudgetCommand.Execute(null);
}
[RelayCommand]
private void EditBudget(Budget budget)
{
((MainViewModel)parentViewModel).OpenEditBudgetCommand.Execute(budget);
}
[RelayCommand]
private void EditSavingsGoal()
{
((MainViewModel)parentViewModel).OpenEditSavingsGoalCommand.Execute(null);
}
private void ProcessChartData() private void ProcessChartData()
{ {
var categories = Categories;
var transactions = Transactions;
var tempCategorySpendingBreakdown = new List<(Category category, double[] spent)>(); var tempCategorySpendingBreakdown = new List<(Category category, double[] spent)>();
var tempSpendingBreakdownLegends = new List<Budget>(); var tempSpendingBreakdownLegends = new List<Budget>();
foreach (var category in categories) foreach (var category in AppData.Categories)
{ {
var spent = transactions var spent = AppData.Transactions
.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase) && .Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase) &&
x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year) x.Date.Month == CurrentPeriod.Month && x.Date.Year == CurrentPeriod.Year)
.Sum(x => x.Amount); .Sum(x => x.ConvertedAmount);
if (spent == 0) continue; if (spent == 0) continue;
double[] values = [(double)spent]; double[] values = [(double)spent];
tempCategorySpendingBreakdown.Add((category, values)); tempCategorySpendingBreakdown.Add((category, values));
@@ -105,7 +136,7 @@ public partial class BudgetViewModel : ViewModelBase
Values = x.spent, Values = x.spent,
Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)), Fill = new SolidColorPaint(SKColor.Parse(x.category.Color)),
InnerRadius = 60, InnerRadius = 60,
ToolTipLabelFormatter = point => $"${point.Coordinate.PrimaryValue:N0}" ToolTipLabelFormatter = point => $"{PrimarySymbol}{point.Coordinate.PrimaryValue:N0}"
}).ToArray(); }).ToArray();
SpendingBreakdownLegends = tempSpendingBreakdownLegends.OrderByDescending(x => x.Spent).ToList(); SpendingBreakdownLegends = tempSpendingBreakdownLegends.OrderByDescending(x => x.Spent).ToList();
@@ -115,11 +146,33 @@ public partial class BudgetViewModel : ViewModelBase
{ {
VisibleBudgets.Clear(); VisibleBudgets.Clear();
VisibleBudgets = new ObservableCollection<Budget>(await DataRepo.General.FetchProcessedBudgets(CurrentPeriod)); VisibleBudgets = new ObservableCollection<Budget>(await DataRepo.General.FetchProcessedBudgets(CurrentPeriod));
_onTrackCount = VisibleBudgets.Count(x => x.IsOnTrack); _onTrackCount = VisibleBudgets.Count(x => x is { IsOnTrack: true, GroupHeader: false });
_approachingCount = VisibleBudgets.Count(x => x.IsWarning); _approachingCount = VisibleBudgets.Count(x => x is { IsWarning: true, GroupHeader: false });
_overBudgetCount = VisibleBudgets.Count(x => x.IsOverBudget); _overBudgetCount = VisibleBudgets.Count(x => x is { IsOverBudget: true, GroupHeader: false });
TotalBudgeted = VisibleBudgets.Sum(x => x.LimitAmount); TotalBudgeted = VisibleBudgets.Sum(x => x.LimitAmount);
TotalSpent = VisibleBudgets.Sum(x => x.Spent); TotalSpent = VisibleBudgets.Sum(x => x.Spent);
NotifyComputedPropertiesOnChanged();
}
private void NotifyComputedPropertiesOnChanged()
{
OnPropertyChanged(nameof(CanGoToNextPeriod));
OnPropertyChanged(nameof(CanGoToPreviousPeriod));
OnPropertyChanged(nameof(CurrentPeriodFormatted));
OnPropertyChanged(nameof(SpentPercentageFormatted));
OnPropertyChanged(nameof(TotalLeft));
OnPropertyChanged(nameof(TotalLeftFormatted));
OnPropertyChanged(nameof(HasSavingsGoal));
OnPropertyChanged(nameof(IsSavingsGoalMet));
OnPropertyChanged(nameof(SavingsHint));
OnPropertyChanged(nameof(OnTrackCountFormatted));
OnPropertyChanged(nameof(ApproachingCountFormatted));
OnPropertyChanged(nameof(OverBudgetCountFormatted));
OnPropertyChanged(nameof(PeriodLength));
OnPropertyChanged(nameof(PeriodDaysPassed));
OnPropertyChanged(nameof(PeriodDaysLeftFormatted));
OnPropertyChanged(nameof(DailyBudgetLeftFormatted));
} }
[RelayCommand(CanExecute = nameof(CanGoToNextPeriod))] [RelayCommand(CanExecute = nameof(CanGoToNextPeriod))]

View File

@@ -5,6 +5,9 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Clario.Data; using Clario.Data;
using Clario.Messages;
using Clario.Services;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Clario.Models; using Clario.Models;
@@ -18,10 +21,8 @@ namespace Clario.ViewModels;
public partial class DashboardViewModel : ViewModelBase public partial class DashboardViewModel : ViewModelBase
{ {
public required ViewModelBase parentViewModel; public required ViewModelBase parentViewModel;
public required List<Transaction> Transactions = new(); public GeneralDataRepo AppData => DataRepo.General;
public required List<Category> Categories = new(); // public required List<Account> Accounts = new();
public required List<Budget> Budgets = new();
public required List<Account> Accounts = new();
[ObservableProperty] private ObservableCollection<ColumnChartData> _spendingByCategoryChartData = new(); [ObservableProperty] private ObservableCollection<ColumnChartData> _spendingByCategoryChartData = new();
[ObservableProperty] private ISeries[] _spendingByCategoryChartSeries = new ISeries[] { }; [ObservableProperty] private ISeries[] _spendingByCategoryChartSeries = new ISeries[] { };
@@ -29,21 +30,40 @@ public partial class DashboardViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<Account> _accountsSummaryData = new(); [ObservableProperty] private ObservableCollection<Account> _accountsSummaryData = new();
[ObservableProperty] private ObservableCollection<Transaction> _recentTransactions = new(); [ObservableProperty] private ObservableCollection<Transaction> _recentTransactions = new();
[ObservableProperty] private decimal _totalNetworth; [ObservableProperty] private decimal _totalNetworth;
public string PrimarySymbol => CurrencyService.GetSymbol(AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD");
[ObservableProperty] private decimal _monthlyIncome; [ObservableProperty] private decimal _monthlyIncome;
private decimal _monthlyIncomeChange; private decimal _monthlyIncomeChange;
private bool _hasLastMonthIncome;
public int MaxChartWidth => SpendingByCategoryChartData.Count * 150; public int MaxChartWidth => SpendingByCategoryChartData.Count * 150;
public string MonthlyIncomeChangeFormatted => _monthlyIncomeChange >= 0 public string MonthlyIncomeChangeFormatted
? "↑" + _monthlyIncomeChange.ToString("0.0%") {
: "↓" + _monthlyIncomeChange.ToString("0.0%"); get
{
if (!_hasLastMonthIncome)
return MonthlyIncome > 0 ? "NEW" : "—";
return _monthlyIncomeChange >= 0
? "↑ " + _monthlyIncomeChange.ToString("0.0%")
: "↓ " + _monthlyIncomeChange.ToString("0.0%");
}
}
[ObservableProperty] private decimal _monthlyExpenses; [ObservableProperty] private decimal _monthlyExpenses;
private decimal _monthlyExpensesChange; private decimal _monthlyExpensesChange;
private bool _hasLastMonthExpenses;
public string MonthlyExpenseChangeFormatted => _monthlyExpensesChange >= 0 public string MonthlyExpenseChangeFormatted
? "↑" + _monthlyExpensesChange.ToString("0.0%") {
: "↓" + _monthlyExpensesChange.ToString("0.0%"); get
{
if (!_hasLastMonthExpenses)
return MonthlyExpenses > 0 ? "NEW" : "—";
return _monthlyExpensesChange >= 0
? "↑ " + _monthlyExpensesChange.ToString("0.0%")
: "↓ " + _monthlyExpensesChange.ToString("0.0%");
}
}
public string AccountsSubtitle => public string AccountsSubtitle =>
AccountsSummaryData.Count == 1 ? $" {AccountsSummaryData.Count} linked Account" : $"{AccountsSummaryData.Count} linked Accounts"; AccountsSummaryData.Count == 1 ? $" {AccountsSummaryData.Count} linked Account" : $"{AccountsSummaryData.Count} linked Accounts";
@@ -61,6 +81,8 @@ public partial class DashboardViewModel : ViewModelBase
}; };
[ObservableProperty] private string _selectedChartTimePeriod = "This Month"; [ObservableProperty] private string _selectedChartTimePeriod = "This Month";
[ObservableProperty] private string _selectedChartTimPeriodSubTitle = DateTime.Now.ToString("MMMM yyyy");
[ObservableProperty] private string _dateToday = DateTime.Now.ToString("dddd, MMMM d, yyyy");
partial void OnSelectedChartTimePeriodChanged(string value) partial void OnSelectedChartTimePeriodChanged(string value)
{ {
@@ -73,11 +95,26 @@ public partial class DashboardViewModel : ViewModelBase
_ => ChartTimePeriod.ThisMonth _ => ChartTimePeriod.ThisMonth
}; };
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(period);
} }
public DashboardViewModel() 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();
WeakReferenceMessenger.Default.Register<RatesRefreshed>(this, (_, _) => UpdateUserOverview());
initialize();
} }
public void initialize() public void initialize()
@@ -88,36 +125,44 @@ public partial class DashboardViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void UpdateUserOverview() private void UpdateUserOverview()
{ {
var thisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); CalculateMonthlyValues();
var lastMonth = thisMonth.AddMonths(-1);
MonthlyIncome = Transactions.Where(x => x.Type == "income" && x.Date.Month == thisMonth.Month && x.Date.Year == thisMonth.Year)
.Sum(x => x.Amount);
MonthlyExpenses = Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month && x.Date.Year == DateTime.Now.Year)
.Sum(x => x.Amount);
var lastMonthIncome = Transactions.Where(x => x.Type == "income" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.Amount);
var lastMonthExpenses = Transactions.Where(x => x.Type == "expense" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.Amount);
try
{
_monthlyIncomeChange = Math.Round((MonthlyIncome / ((lastMonthIncome == 0) ? 1 : lastMonthIncome)) - 1, 2);
_monthlyExpensesChange = Math.Round((MonthlyExpenses / ((lastMonthExpenses == 0) ? 1 : lastMonthExpenses)) - 1, 2);
}
catch (Exception e)
{
Console.WriteLine(e);
}
OnPropertyChanged(nameof(MonthlyIncomeChangeFormatted));
OnPropertyChanged(nameof(MonthlyExpenseChangeFormatted));
UpdateSpendingByCategoryChart(); UpdateSpendingByCategoryChart();
_ = UpdateBudgetTracker(); _ = UpdateBudgetTracker();
UpdateRecentTransactions(); UpdateRecentTransactions();
UpdateAccountsSummary(); UpdateAccountsSummary();
} }
private void CalculateMonthlyValues()
{
var thisMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
var lastMonth = thisMonth.AddMonths(-1);
MonthlyIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == thisMonth.Month && x.Date.Year == thisMonth.Year)
.Sum(x => x.ConvertedAmount);
MonthlyExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == DateTime.Now.Month && x.Date.Year == DateTime.Now.Year)
.Sum(x => x.ConvertedAmount);
var lastMonthIncome = AppData.Transactions.Where(x => x.Type == "income" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.ConvertedAmount);
var lastMonthExpenses = AppData.Transactions.Where(x => x.Type == "expense" && x.Date.Month == lastMonth.Month && x.Date.Year == lastMonth.Year)
.Sum(x => x.ConvertedAmount);
_hasLastMonthIncome = lastMonthIncome > 0;
_hasLastMonthExpenses = lastMonthExpenses > 0;
if (_hasLastMonthIncome)
{
_monthlyIncomeChange = Math.Round((MonthlyIncome / lastMonthIncome) - 1, 2);
}
if (_hasLastMonthExpenses)
{
_monthlyExpensesChange = Math.Round((MonthlyExpenses / lastMonthExpenses) - 1, 2);
}
OnPropertyChanged(nameof(MonthlyIncomeChangeFormatted));
OnPropertyChanged(nameof(MonthlyExpenseChangeFormatted));
}
[RelayCommand] [RelayCommand]
private void ViewAllTransactions() private void ViewAllTransactions()
{ {
@@ -137,10 +182,10 @@ public partial class DashboardViewModel : ViewModelBase
{ {
var tempList = new List<ColumnChartData>(); var tempList = new List<ColumnChartData>();
foreach (var category in Categories) foreach (var category in AppData.Categories)
{ {
var categoryTransactions = var categoryTransactions =
Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase)); AppData.Transactions.Where(x => x.CategoryId == category.Id && x.Type.Equals("expense", StringComparison.OrdinalIgnoreCase));
switch (period) switch (period)
{ {
@@ -167,7 +212,7 @@ public partial class DashboardViewModel : ViewModelBase
break; break;
} }
var balance = categoryTransactions.Sum(x => x.Amount); var balance = categoryTransactions.Sum(x => x.ConvertedAmount);
if (balance == 0) continue; if (balance == 0) continue;
tempList.Add(new ColumnChartData() 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)balance], Fill = new SolidColorPaint(SKColor.Parse(category.Color)) });
@@ -196,20 +241,25 @@ public partial class DashboardViewModel : ViewModelBase
private void UpdateRecentTransactions() private void UpdateRecentTransactions()
{ {
RecentTransactions = new ObservableCollection<Transaction>(Transactions.OrderByDescending(x => x.Date).Take(5)); RecentTransactions = new ObservableCollection<Transaction>(AppData.Transactions.OrderByDescending(x => x.Date).Take(5));
OnPropertyChanged(nameof(HasTransactionData)); OnPropertyChanged(nameof(HasTransactionData));
} }
private void UpdateAccountsSummary() private void UpdateAccountsSummary()
{ {
foreach (var account in Accounts) TotalNetworth = 0;
var primaryCurrency = AppData.PrimaryAccount?.Currency ?? AppData.Profile?.Currency ?? "USD";
foreach (var account in AppData.Accounts)
{ {
var accountTransactions = Transactions.Where(t => t.AccountId == account.Id).ToList(); 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 == "income" ? t.Amount : -t.Amount);
if (account.Currency.Equals(primaryCurrency, StringComparison.OrdinalIgnoreCase))
TotalNetworth += account.CurrentBalance; TotalNetworth += account.CurrentBalance;
else
TotalNetworth += accountTransactions.Sum(t => t.Type == "income" ? t.ConvertedAmount : -t.ConvertedAmount);
} }
AccountsSummaryData = new ObservableCollection<Account>(Accounts.OrderBy(x => x.CreatedAt)); AccountsSummaryData = new ObservableCollection<Account>(AppData.Accounts.OrderBy(x => x.CreatedAt));
OnPropertyChanged(nameof(AccountsSubtitle)); OnPropertyChanged(nameof(AccountsSubtitle));
} }

Some files were not shown because too many files have changed in this diff Show More