22 Commits
V0.3.0 ... main

Author SHA1 Message Date
d6efa72745 updated README.md 2026-04-11 00:42:53 +03:00
2ce47ee305 updated README.md 2026-04-11 00:40:14 +03:00
90b2abd587 stuff
Some checks failed
Build Linux / build (push) Failing after 23s
2026-04-09 23:56:34 +03:00
61ff949c19 Add analytics page, auth error handling, and period navigation fix
Some checks failed
Build Linux / build (push) Failing after 24s
Features
Analytics Page: Full-featured analytics dashboard with KPI cards, cash flow trend chart, net worth progression, spending patterns by day-of-week, top spending categories, and income sources breakdown. Includes PDF export via QuestPDF for selected periods. Implemented on both desktop and mobile (simplified).
Auth Error Handling: Map Supabase GotrueException errors to AuthError enum with user-friendly messages for login and signup. Display errors in sign-in and sign-up panels.
Dynamic Transaction/Account Counts: Replace hardcoded "46 transactions" and "4 accounts" text with FilteredTransactionCount and ActiveAccountCount properties bound to actual data.

Fixes
Budget Period Navigation: Fix year-aware date comparison in CanGoToPreviousPeriod and CanGoToNextPeriod. Previously only compared months, preventing navigation before January of current year.

Changes
AnalyticsViewModel: Period selector, KPI calculations, chart data builders (cash flow, net worth, day-of-week, top categories, income sources), PDF export
PdfExportService: QuestPDF report generation with print-optimized styling
AuthViewModel: Error display with GotrueException mapping
BudgetViewModel: Year-aware period navigation
TransactionsViewModel: FilteredTransactionCount property
AccountsViewModel: ActiveAccountCount property
MainViewModel: Analytics navigation and AnalyticsViewModel integration
Views: Analytics button wired, error messages displayed, count bindings updated
2026-04-05 23:08:34 +03:00
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
221 changed files with 17597 additions and 2253 deletions

View File

@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(chmod +x \"/c/Users/Nouredeen/.claude/scripts/context-bar.sh\")",
"Bash(dotnet build:*)",
"WebFetch(domain:git.nouredeen.dev)",
"WebFetch(domain:supabase.com)",
"Bash(grep:*)",
"Bash(cmd:*)"
]
},
"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

@@ -1,42 +0,0 @@
name: Build Linux
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore Clario/Clario.csproj
- name: Build
run: dotnet build Clario/Clario.csproj --configuration Release --no-restore
- name: Publish
run: dotnet publish Clario/Clario.csproj \
--configuration Release \
--runtime linux-x64 \
--self-contained true \
--output ./publish/linux \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Clario-linux-x64
path: ./publish/linux
retention-days: 7

9
.gitignore vendored
View File

@@ -3,4 +3,11 @@ obj/
.vs/
.idea/
*.user
*.suo
*.suo
./Clario/CLAUDE_CONTEXT.md
publish/
*.tar.gz
Clario/devsettings.json
.env
TODO.md
clario.keystore

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,14 @@
<ItemGroup>
<PackageReference Include="Avalonia.Android"/>
<PackageReference Include="Avalonia.Controls.ColorPicker" />
<PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen"/>

View File

@@ -1,21 +1,50 @@
using Android.App;
using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Avalonia;
using Avalonia.Android;
using Clario;
namespace Clario.Android;
[Activity(
Label = "Clario.Android",
Label = "Clario",
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = "clario",
DataHost = "auth")]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override void OnCreate(Bundle? savedInstanceState)
{
// Capture deep link before Avalonia initializes
var uri = Intent?.DataString;
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
App.PendingDeepLink = uri;
base.OnCreate(savedInstanceState);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
// Called when app is already running (SingleTop) and link is opened again
var uri = intent?.DataString;
if (uri?.StartsWith("clario://", StringComparison.OrdinalIgnoreCase) == true)
_ = App.HandleDeepLink(uri);
}
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.WithInterFont();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
{
"GeneralSettings": {
"NetProjectPath": "Clario.Desktop.csproj",
"ApplicationName": "Clario",
"Version": "0.6.0",
"PackageName": {
"$type": "msbuild",
"property": "AssemblyName"
},
"AssemblyName": {
"$type": "msbuild",
"property": "AssemblyName"
}
},
"LinuxSettings": {
"AppIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
"CreateBinSymlink": "True"
},
"Win32Settings": {
"InstallerIcon": "../Clario/Assets/AppIcons/logo-icon-primary-transparent.ico",
"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

@@ -1,19 +1,25 @@
using System;
using System;
using System.Linq;
using Avalonia;
using Clario;
namespace Clario.Desktop;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Capture deep link passed as command-line arg by Windows protocol handler
var deepLink = args.FirstOrDefault(a =>
a.StartsWith("clario://", StringComparison.OrdinalIgnoreCase));
if (deepLink != null)
App.PendingDeepLink = deepLink;
// Register clario:// URL scheme on Windows (idempotent)
RegisterUrlScheme();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
@@ -22,4 +28,23 @@ sealed class Program
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
private static void RegisterUrlScheme()
{
if (!OperatingSystem.IsWindows()) return;
try
{
var exe = Environment.ProcessPath
?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
if (exe is null) return;
using var key = Microsoft.Win32.Registry.CurrentUser
.CreateSubKey(@"SOFTWARE\Classes\clario");
key.SetValue("", "URL:Clario Protocol");
key.SetValue("URL Protocol", "");
using var cmd = key.CreateSubKey(@"shell\open\command");
cmd.SetValue("", $"\"{exe}\" \"%1\"");
}
catch { /* ignore — no registry write access in sandboxed environments */ }
}
}

View File

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

View File

@@ -28,10 +28,23 @@
<converters:DecimalColorConverter x:Key="DecimalColorConverter" />
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:BoolToCssConverter x:Key="BoolToCssConverter" />
<converters:CreditAmountConverter x:Key="CreditAmountConverter"/>
<converters:BoolToStringConverter x:Key="BoolToStringConverter"/>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="../Theme/AppTheme.axaml" />
<StyleInclude Source="avares://AvaloniaProgressRing/Styles/ProgressRing.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="avares://FluentAvalonia.ProgressRing/Styling/Controls/ProgressRing.axaml" />
<!-- Must come after ColorPicker Fluent.xaml to override Width="64" setter -->
<Styles>
<Style Selector="ColorPicker">
<Setter Property="Width" Value="NaN" />
</Style>
<Style Selector="ColorPicker /template/ DropDownButton">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</Styles>
</Application.Styles>
</Application>

View File

@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
@@ -17,6 +18,47 @@ public partial class App : Application
{
public static bool IsMobile { get; private set; }
/// <summary>Set before OnFrameworkInitializationCompleted runs (from Program.cs or MainActivity).</summary>
public static string? PendingDeepLink { get; set; }
/// <summary>Called from MainActivity.OnNewIntent when app is already running.</summary>
public static async Task HandleDeepLink(string deepLink)
{
var (accessToken, refreshToken, type) = ParseDeepLinkFragment(deepLink);
if (type != "recovery" || accessToken is null) return;
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
var vm = new ResetPasswordViewModel();
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow!.DataContext = vm;
else if (Current?.ApplicationLifetime is ISingleViewApplicationLifetime sv)
sv.MainView!.DataContext = vm;
});
}
private static (string? accessToken, string? refreshToken, string? type) ParseDeepLinkFragment(string url)
{
var hash = url.IndexOf('#');
if (hash < 0) return default;
string? at = null, rt = null, type = null;
foreach (var part in url[(hash + 1)..].Split('&'))
{
var eq = part.IndexOf('=');
if (eq < 0) continue;
var val = Uri.UnescapeDataString(part[(eq + 1)..]);
switch (part[..eq])
{
case "access_token": at = val; break;
case "refresh_token": rt = val; break;
case "type": type = val; break;
}
}
return (at, rt, type);
}
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -42,7 +84,7 @@ public partial class App : Application
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatformLoading)
{
Console.WriteLine("ANDROID PATH HIT");
DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatformLoading.MainView = new MainAppMobile()
{
DataContext = new LoadingViewModel()
@@ -50,7 +92,7 @@ public partial class App : Application
}
IsMobile = ApplicationLifetime is ISingleViewApplicationLifetime;
var culture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = culture;
@@ -60,8 +102,9 @@ public partial class App : Application
{
await SupabaseService.Client.Auth.RetrieveSessionAsync();
}
catch
catch (Exception e)
{
DebugLogger.Log($"[Auth] RetrieveSession failed: {e.Message}");
}
var user = SupabaseService.Client.Auth.CurrentUser;
@@ -72,19 +115,32 @@ public partial class App : Application
ThemeService.SwitchToTheme(profile.Theme);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
// Check for deep link from password reset email
ViewModelBase targetViewModel;
if (PendingDeepLink is { } deepLink && deepLink.Contains("type=recovery"))
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
var (accessToken, refreshToken, _) = ParseDeepLinkFragment(deepLink);
if (accessToken is not null)
{
try { await SupabaseService.Client.Auth.SetSession(accessToken, refreshToken); } catch { }
}
PendingDeepLink = null;
targetViewModel = new ResetPasswordViewModel();
}
else
{
targetViewModel = user is not null ? new MainViewModel() : new AuthViewModel();
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow!.DataContext = targetViewModel;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
Console.WriteLine("ANDROID PATH HIT");
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
DebugLogger.Log("ANDROID PATH HIT");
singleViewPlatform.MainView!.DataContext = targetViewModel;
}
}

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,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="18.5" cy="17.5" r="3.5" />
<circle cx="5.5" cy="17.5" r="3.5" />
<circle cx="15" cy="5" r="1" />
<path d="M12 17.5V14l-3-3 4-3 2 3h2" />
</svg>

After

Width:  |  Height:  |  Size: 365 B

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 7v14" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 6v6" />
<path d="M15 6v6" />
<path d="M2 12h19.6" />
<path d="M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3" />
<circle cx="7" cy="18" r="2" />
<path d="M9 18h5" />
<circle cx="16" cy="18" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 492 B

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="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
<circle cx="12" cy="13" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

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,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 2v2" />
<path d="M14 2v2" />
<path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1" />
<path d="M6 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z" />
<path d="m2.5 21.5 1.4-1.4" />
<path d="m20.1 3.9 1.4-1.4" />
<path d="M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z" />
<path d="m9.6 14.4 4.8-4.8" />
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@@ -0,0 +1,20 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

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="M12 7v14" />
<path d="M20 11v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8" />
<path d="M7.5 7a1 1 0 0 1 0-5A4.8 8 0 0 1 12 7a4.8 8 0 0 1 4.5-5 1 1 0 0 1 0 5" />
<rect x="3" y="7" width="18" height="4" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 426 B

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="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z" />
<path d="M22 10v6" />
<path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5" />
</svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" />
</svg>

After

Width:  |  Height:  |  Size: 347 B

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="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z" />
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
</svg>

After

Width:  |  Height:  |  Size: 368 B

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,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 308 B

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="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
<path d="M12 22V12" />
<polyline points="3.29 7 12 12 20.71 7" />
<path d="m7.5 4.27 9 5.15" />
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 14-1 1" />
<path d="m13.75 18.25-1.25 1.42" />
<path d="M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12" />
<path d="M18.8 9.3a1 1 0 0 0 2.1 7.7" />
<path d="M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z" />
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
</svg>

After

Width:  |  Height:  |  Size: 421 B

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,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="6" cy="6" r="3" />
<path d="M8.12 8.12 12 12" />
<path d="M20 4 8.12 15.88" />
<circle cx="6" cy="18" r="3" />
<path d="M14.8 14.8 20 20" />
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z" />
</svg>

After

Width:  |  Height:  |  Size: 401 B

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"
>
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
<path d="M12 18h.01" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 2v2" />
<path d="M5 2v2" />
<path d="M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1" />
<path d="M8 15a6 6 0 0 0 12 0v-3" />
<circle cx="20" cy="10" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 399 B

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,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 3.1V7a4 4 0 0 0 8 0V3.1" />
<path d="m9 15-1-1" />
<path d="m15 15 1-1" />
<path d="M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z" />
<path d="m8 19-2 3" />
<path d="m16 19 2 3" />
</svg>

After

Width:  |  Height:  |  Size: 427 B

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="m17 2-5 5-5-5" />
<rect width="20" height="15" x="2" y="7" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 290 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

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="M8 22h8" />
<path d="M7 10h10" />
<path d="M12 15v7" />
<path d="M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" />
</svg>

After

Width:  |  Height:  |  Size: 417 B

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

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net8.0-android</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
@@ -13,7 +13,8 @@
<ItemGroup>
<PackageReference Include="Avalonia"/>
<PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="Avalonia.Controls.ColorPicker"/>
<PackageReference Include="Avalonia.Svg.Skia"/>
<PackageReference Include="Avalonia.Themes.Fluent"/>
<PackageReference Include="Avalonia.Fonts.Inter"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@@ -22,19 +23,48 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm"/>
<PackageReference Include="Deadpikle.AvaloniaProgressRing" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" />
<PackageReference Include="Supabase" />
<PackageReference Include="Xaml.Behaviors.Interactions" />
<PackageReference Include="Xaml.Behaviors.Interactivity" />
<PackageReference Include="Deadpikle.AvaloniaProgressRing"/>
<PackageReference Include="FluentAvalonia.ProgressRing"/>
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia"/>
<PackageReference Include="QuestPDF" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly"/>
<PackageReference Include="Supabase"/>
<PackageReference Include="Xaml.Behaviors.Interactions"/>
<PackageReference Include="Xaml.Behaviors.Interactivity"/>
<PackageReference Include="SkiaSharp"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies"/>
</ItemGroup>
<ItemGroup>
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
<DependentUpon>MobileMainView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="MobileViews\MainAppMobile.axaml.cs">
<DependentUpon>MobileMainView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\AccountFormView.axaml.cs">
<DependentUpon>AccountFormView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\CategoryFormView.axaml.cs">
<DependentUpon>CategoryFormView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\AnalyticsView.axaml.cs">
<DependentUpon>AnalyticsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\DashboardSkeletonView.axaml.cs">
<DependentUpon>DashboardSkeletonView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<None Update="devsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
<AndroidKeyStore>true</AndroidKeyStore>
<AndroidSigningKeyStore>clario.keystore</AndroidSigningKeyStore>
<AndroidSigningKeyAlias>clario</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningKeyPass>
<AndroidSigningStorePass>env:ANDROID_SIGNING_PASSWORD</AndroidSigningStorePass>
</PropertyGroup>
</Project>

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.Globalization;
using Avalonia.Controls.Converters;
using Avalonia.Data.Converters;
using Avalonia.Media;
@@ -21,6 +22,18 @@ public class HexToColorConverter : IValueConverter
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)
{
if (value is null || value.Count < 2 || value.Any(x => x is null)) return 0;
if (value[0] is double incomeD && (value[1] is double expenseD))
if (value is null || value.Count < 2) return 0;
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;
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;
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;

View File

@@ -2,6 +2,76 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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">
<Setter Property="MinHeight" Value="15" />
<Setter Property="MinWidth" Value="50" />
@@ -14,10 +84,9 @@
<Setter Property="Template">
<ControlTemplate>
<Grid>
<!-- Trigger button -->
<Button x:Name="PART_Button"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
BorderBrush="{TemplateBinding BorderBrush}"
@@ -48,7 +117,6 @@
</Grid>
</Button>
<!-- Popup -->
<Popup x:Name="PART_Popup"
PlacementTarget="{Binding #PART_Button}"
Placement="Bottom"
@@ -65,21 +133,50 @@
BorderThickness="0" />
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- pointerover -->
<Style Selector="local|DateRangePicker:pointerover /template/ Button#PART_Button">
<Setter Property="Background" Value="{DynamicResource BgHover}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderAccent}" />
</Style>
<!-- pressed -->
<Style Selector="local|DateRangePicker:pressed /template/ Button#PART_Button">
<Setter Property="Background" Value="{DynamicResource BorderSubtle}" />
</Style>
</Styles>
<!-- ============================================================ -->
<!-- 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>

View File

@@ -5,8 +5,10 @@ using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Calendar = Avalonia.Controls.Calendar;
namespace Clario.CustomControls;
@@ -23,19 +25,26 @@ public class DateRangePicker : TemplatedControl
set => SetValue(SelectionModeProperty, value);
}
public static readonly StyledProperty<IList<DateTime>> SelectedDatesProperty =
AvaloniaProperty.Register<DateRangePicker, IList<DateTime>>(
nameof(SelectedDates), new List<DateTime>());
private IList<DateTime> _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
{
get => GetValue(SelectedDatesProperty);
set => SetValue(SelectedDatesProperty, value);
get => _selectedDates;
set => SetAndRaise(SelectedDatesProperty, ref _selectedDates, value);
}
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedDate), null);
nameof(SelectedDate),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay);
public DateTime? SelectedDate
{
@@ -58,7 +67,6 @@ public class DateRangePicker : TemplatedControl
private Popup? _popup;
private Calendar? _calendar;
private bool _isSyncing = false;
@@ -66,12 +74,12 @@ public class DateRangePicker : TemplatedControl
{
base.OnApplyTemplate(e);
if (_button != null) _button.Click -= OnButtonClick;
if (_calendar != null)
{
_calendar.SelectedDatesChanged -= OnCalendarDatesChanged;
_calendar.RemoveHandler(PointerReleasedEvent, OnCalendarPointerReleased);
// _calendar.RemoveHandler(Button.ClickEvent, OnCalendarInternalClick); // add this
}
_button = e.NameScope.Find<Button>("PART_Button");
@@ -85,10 +93,9 @@ public class DateRangePicker : TemplatedControl
{
_calendar.AllowTapRangeSelection = true;
_calendar.SelectedDatesChanged += OnCalendarDatesChanged;
_calendar.AddHandler(PointerReleasedEvent, OnCalendarPointerReleased, RoutingStrategies.Tunnel);
// _calendar.AddHandler(Button.ClickEvent, OnCalendarInternalClick, RoutingStrategies.Tunnel);
SyncToCalendar();
}
@@ -96,27 +103,31 @@ public class DateRangePicker : TemplatedControl
UpdateDisplayText();
}
private void OnCalendarInternalClick(object? sender, RoutedEventArgs e)
{
e.Handled = true;
}
private void OnCalendarPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_calendar!.SelectionMode != CalendarSelectionMode.SingleDate) return;
if (_isSyncing) return;
if (_popup is null || !_popup.IsOpen) return;
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();
_isSyncing = true;
try
{
SelectedDates = newDates;
SelectedDate = newDates.Count > 0 ? newDates[0] : null;
UpdateDisplayText();
bool shouldClose = SelectionMode switch
{
CalendarSelectionMode.SingleDate => newDates.Count >= 1,
@@ -133,12 +144,10 @@ public class DateRangePicker : TemplatedControl
}
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
if (_popup is null) return;
SyncToCalendar();
_popup.IsOpen = true;
}
@@ -157,13 +166,10 @@ public class DateRangePicker : TemplatedControl
try
{
SelectedDates = newDates;
SelectedDate = newDates.Count > 0 ? newDates[0] : null;
UpdateDisplayText();
bool shouldClose = SelectionMode switch
{
CalendarSelectionMode.SingleDate => newDates.Count >= 1,
@@ -180,7 +186,6 @@ public class DateRangePicker : TemplatedControl
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -220,7 +225,6 @@ public class DateRangePicker : TemplatedControl
}
}
private void SyncToCalendar()
{
if (_calendar is null || _isSyncing) return;

View File

@@ -1,49 +1,75 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Clario.Models;
using Clario.Models.GeneralModels;
using Clario.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Clario.Messages;
using CommunityToolkit.Mvvm.Messaging;
using Supabase.Postgrest;
using Supabase.Realtime.PostgresChanges;
using Constants = Supabase.Realtime.Constants;
using FileOptions = Supabase.Storage.FileOptions;
namespace Clario.Data;
public class GeneralDataRepo
public record ProfileUpdated();
public partial class GeneralDataRepo : ObservableObject
{
public Profile? Profile { get; set; }
public List<Category>? Categories { get; set; }
public List<Account>? Accounts { get; set; }
public List<Budget>? Budgets { get; set; }
public List<Transaction>? Transactions { get; set; }
[ObservableProperty] private Profile? _profile;
[ObservableProperty] private ObservableCollection<Category> _categories = new();
[ObservableProperty] private ObservableCollection<Account> _accounts = new();
[ObservableProperty] private ObservableCollection<Budget> _budgets = new();
[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();
if (profile.Models.Count == 0) return null;
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);
}
catch (Exception e)
{
Console.WriteLine(e);
return;
var bytes = await _HttpClient.GetByteArrayAsync(url);
var stream = new MemoryStream(bytes);
Profile.Avatar = new Bitmap(stream);
}
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();
Transactions = transactions.Models;
Transactions = new ObservableCollection<Transaction>(transactions.Models);
return transactions.Models;
}
@@ -51,11 +77,19 @@ public class GeneralDataRepo
{
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)
{
Console.WriteLine(e);
DebugLogger.Log(e);
return;
}
}
@@ -63,11 +97,22 @@ public class GeneralDataRepo
{
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)
{
Console.WriteLine(e);
DebugLogger.Log(e);
}
}
@@ -76,68 +121,242 @@ public class GeneralDataRepo
try
{
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)
{
Console.WriteLine(e);
DebugLogger.Log(e);
throw;
}
}
public async Task<List<Category>> FetchCategories()
public async Task InsertTransfer(Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
{
if (Categories is not null) return Categories;
try
{
var userId = Guid.Parse(SupabaseService.Client.Auth.CurrentUser!.Id!);
var pairId = Guid.NewGuid();
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
var outTx = new Transaction
{
Id = Guid.NewGuid(),
UserId = userId,
AccountId = fromAccountId,
Type = "transfer_out",
Amount = amount,
Description = "Transfer",
Note = note?.Trim(),
Date = date,
TransferPairId = pairId,
};
var inTx = new Transaction
{
Id = Guid.NewGuid(),
UserId = userId,
AccountId = toAccountId,
Type = "transfer_in",
Amount = toAmount,
Description = "Transfer",
Note = note?.Trim(),
Date = date,
TransferPairId = pairId,
};
var outResult = await SupabaseService.Client.From<Transaction>().Insert(outTx);
var inResult = await SupabaseService.Client.From<Transaction>().Insert(inTx);
if (outResult.Models.Count >= 1)
{
var enriched = LinkTransactionCategories(outResult.Models[0]);
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
if (inResult.Models.Count >= 1)
{
var enriched = LinkTransactionCategories(inResult.Models[0]);
LinkTransactionAccounts(enriched);
Transactions.Add(enriched);
}
// Re-enrich both so AccountDisplayText can reference the counterpart (from/to)
LinkTransactionAccounts();
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task UpdateTransfer(Guid transferPairId, Guid fromAccountId, Guid toAccountId, decimal amount, DateTime date, string? note)
{
try
{
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
var fromCurrency = Accounts.FirstOrDefault(a => a.Id == fromAccountId)?.Currency ?? "";
var toCurrency = Accounts.FirstOrDefault(a => a.Id == toAccountId)?.Currency ?? "";
var toAmount = ConvertAmount(amount, fromCurrency, toCurrency);
foreach (var tx in pair)
{
tx.AccountId = tx.Type == "transfer_out" ? fromAccountId : toAccountId;
tx.Amount = tx.Type == "transfer_in" ? toAmount : amount;
tx.Date = date;
tx.Note = note?.Trim();
var result = await SupabaseService.Client.From<Transaction>().Update(tx);
if (result.Model is null) continue;
var index = Transactions.IndexOf(tx);
if (index != -1)
{
var enriched = LinkTransactionCategories(result.Model);
LinkTransactionAccounts(enriched);
Transactions[index] = enriched;
}
}
LinkTransactionAccounts();
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task DeleteTransfer(Guid transferPairId)
{
try
{
await SupabaseService.Client.From<Transaction>()
.Where(x => x.TransferPairId == transferPairId)
.Delete();
var pair = Transactions.Where(t => t.TransferPairId == transferPairId).ToList();
foreach (var tx in pair)
Transactions.Remove(tx);
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task<List<Category>> FetchCategories(bool forceRefresh = false)
{
if (Categories.Count != 0 && !forceRefresh) return Categories.ToList();
var categories = await SupabaseService.Client.From<Category>().Get();
Categories = categories.Models;
Categories = new ObservableCollection<Category>(categories.Models);
return categories.Models;
}
public async Task<List<Account>> FetchAccounts()
public async Task<Category?> InsertCategory(Category category)
{
if (Accounts is not null) return Accounts;
var accounts = await SupabaseService.Client.From<Account>().Get();
Accounts = accounts.Models;
return accounts.Models;
try
{
var result = await SupabaseService.Client.From<Category>()
.Insert(category, new QueryOptions() { Returning = QueryOptions.ReturnType.Representation });
if (result.Model is null) return null;
Categories.Add(result.Model);
return result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
return null;
}
}
public async Task<List<Budget>> FetchBudgets()
public async Task UpdateCategory(Category category)
{
if (Budgets is not null) return Budgets;
try
{
var result = await SupabaseService.Client.From<Category>().Update(category);
if (result.Model is null) return;
var item = Categories.FirstOrDefault(x => x.Id == result.Model.Id);
if (item is null) return;
var index = Categories.IndexOf(item);
if (index != -1) Categories[index] = result.Model;
}
catch (Exception e)
{
DebugLogger.Log(e);
}
}
public async Task DeleteCategory(Guid id)
{
try
{
await SupabaseService.Client.From<Category>().Where(x => x.Id == id).Delete();
var item = Categories.FirstOrDefault(x => x.Id == id);
if (item is null) return;
Categories.Remove(item);
}
catch (Exception e)
{
DebugLogger.Log(e);
throw;
}
}
public async Task<List<Account>> FetchAccounts(bool forceRefresh = false)
{
if (Accounts.Count != 0 && !forceRefresh) return Accounts.ToList();
var accounts = await SupabaseService.Client.From<Account>().Get();
Accounts = new ObservableCollection<Account>(accounts.Models);
return accounts.Models.OrderBy(x => x.IsPrimary).ThenBy(x => x.CreatedAt).ToList();
}
public async Task<List<Budget>> FetchBudgets(bool forceRefresh = false)
{
if (Budgets.Count != 0 && !forceRefresh) return Budgets.ToList();
var budgets = await SupabaseService.Client.From<Budget>().Get();
Budgets = budgets.Models;
Budgets = new ObservableCollection<Budget>(budgets.Models);
return budgets.Models;
}
public async Task<List<Budget>> FetchProcessedBudgets(DateTime CurrentPeriod)
{
var categories = await FetchCategories();
var transactions = await FetchTransactions();
var budgets = await FetchBudgets();
var budgets = Budgets;
var outputList = new List<Budget>();
var primarySymbol = CurrencyService.GetSymbol(PrimaryAccount?.Currency ?? Profile?.Currency ?? "USD");
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())
{
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();
budget.Spent = budgetTransactions.Sum(x => x.Amount);
budget.Spent = budgetTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = budgetTransactions.Count;
break;
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();
budget.Spent = quarterTransactions.Sum(x => x.Amount);
budget.Spent = quarterTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = quarterTransactions.Count;
break;
case "yearly":
var yearTransactions = transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = yearTransactions.Sum(x => x.Amount);
var yearTransactions = Transactions.Where(x => x.Date.Year == CurrentPeriod.Year && x.CategoryId == budget.CategoryId).ToList();
budget.Spent = yearTransactions.Sum(x => x.ConvertedAmount);
budget.TransactionsCount = yearTransactions.Count;
break;
}
OnPropertyChanged(nameof(budget.IsOnTrack));
OnPropertyChanged(nameof(budget.IsWarning));
OnPropertyChanged(nameof(budget.IsOverBudget));
}
@@ -173,4 +392,519 @@ public class GeneralDataRepo
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 is "income" or "transfer_in" ? 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());
}
/// Converts <paramref name="amount"/> from <paramref name="fromCurrency"/> to
/// <paramref name="toCurrency"/> using the current live rates.
/// Falls back to <paramref name="amount"/> unchanged when currencies match or rates are missing.
private static decimal ConvertAmount(decimal amount, string fromCurrency, string toCurrency)
{
if (string.IsNullOrEmpty(fromCurrency) || string.IsNullOrEmpty(toCurrency)) return amount;
if (fromCurrency.Equals(toCurrency, StringComparison.OrdinalIgnoreCase)) return amount;
if (!CurrencyService.LiveRates.TryGetValue(fromCurrency, out var fromRate)) return amount;
if (!CurrencyService.LiveRates.TryGetValue(toCurrency, out var toRate) || toRate == 0) return amount;
// fromRate = 1 fromCurrency in primary; toRate = 1 toCurrency in primary
// amount * fromRate / toRate = amount converted to toCurrency
return Math.Round(amount * fromRate / toRate, 6);
}
public void LinkTransactionCategories()
{
foreach (var transaction in Transactions)
{
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;
if (tx.IsTransfer && tx.TransferPairId.HasValue)
{
var counterpart = Transactions.FirstOrDefault(t => t.TransferPairId == tx.TransferPairId && t.Id != tx.Id);
var counterpartAccount = counterpart is not null ? Accounts.FirstOrDefault(a => a.Id == counterpart.AccountId) : null;
var fromName = tx.IsTransferOut ? (account?.Name ?? "?") : (counterpartAccount?.Name ?? "?");
var toName = tx.IsTransferOut ? (counterpartAccount?.Name ?? "?") : (account?.Name ?? "?");
tx.AccountDisplayText = $"{fromName} → {toName}";
}
else
{
tx.AccountDisplayText = account?.Name ?? "";
}
}
public async Task UpdateSavingsGoal(decimal? goal)
{
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}";
}
public void StartRealtimeSync()
{
if (SupabaseService.Client.Auth.CurrentUser?.Id is null) return;
DebugLogger.Log("[Realtime] StartRealtimeSync: registering listeners");
// Transactions
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedTransaction = c.Model<Transaction>();
if (insertedTransaction is null) { DebugLogger.Log("[Realtime] Transaction INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction INSERT: {insertedTransaction.Id} ({insertedTransaction.Description})");
Dispatcher.UIThread.Post(() =>
{
if (Transactions.Any(x => x.Id == insertedTransaction.Id)) { DebugLogger.Log($"[Realtime] Transaction INSERT: skipped duplicate {insertedTransaction.Id}"); return; }
LinkTransactionCategories(insertedTransaction);
LinkTransactionAccounts(insertedTransaction);
Transactions.Add(insertedTransaction);
DebugLogger.Log($"[Realtime] Transaction INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedTransaction = c.Model<Transaction>();
if (updatedTransaction is null) { DebugLogger.Log("[Realtime] Transaction UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction UPDATE: {updatedTransaction.Id} ({updatedTransaction.Description})");
Dispatcher.UIThread.Post(() =>
{
var idx = Transactions.ToList().FindIndex(x => x.Id == updatedTransaction.Id);
if (idx == -1) { DebugLogger.Log($"[Realtime] Transaction UPDATE: id {updatedTransaction.Id} not found in collection"); return; }
LinkTransactionCategories(updatedTransaction);
LinkTransactionAccounts(updatedTransaction);
Transactions[idx] = updatedTransaction;
DebugLogger.Log($"[Realtime] Transaction UPDATE: replaced at index {idx}");
});
});
_ = SupabaseService.Client.From<Transaction>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedTransaction = c.OldModel<Transaction>();
if (deletedTransaction is null) { DebugLogger.Log("[Realtime] Transaction DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Transaction DELETE: {deletedTransaction.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Transactions.FirstOrDefault(x => x.Id == deletedTransaction.Id);
if (item is not null) { Transactions.Remove(item); DebugLogger.Log($"[Realtime] Transaction DELETE: removed {deletedTransaction.Id}"); }
else DebugLogger.Log($"[Realtime] Transaction DELETE: id {deletedTransaction.Id} not found (already removed locally)");
});
});
// Accounts
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedAccount = c.Model<Account>();
if (insertedAccount is null) { DebugLogger.Log("[Realtime] Account INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Account INSERT: {insertedAccount.Id} ({insertedAccount.Name})");
Dispatcher.UIThread.Post(() =>
{
if (Accounts.Any(x => x.Id == insertedAccount.Id)) { DebugLogger.Log($"[Realtime] Account INSERT: skipped duplicate {insertedAccount.Id}"); return; }
Accounts.Add(insertedAccount);
DebugLogger.Log($"[Realtime] Account INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedAccount = c.Model<Account>();
if (updatedAccount is null) { DebugLogger.Log("[Realtime] Account UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Account UPDATE: {updatedAccount.Id} ({updatedAccount.Name})");
Dispatcher.UIThread.Post(() =>
{
var idx = Accounts.ToList().FindIndex(x => x.Id == updatedAccount.Id);
if (idx != -1) { Accounts[idx] = updatedAccount; DebugLogger.Log($"[Realtime] Account UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Account UPDATE: id {updatedAccount.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Account>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedAccount = c.OldModel<Account>();
if (deletedAccount is null) { DebugLogger.Log("[Realtime] Account DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Account DELETE: {deletedAccount.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Accounts.FirstOrDefault(x => x.Id == deletedAccount.Id);
if (item is not null) { Accounts.Remove(item); DebugLogger.Log($"[Realtime] Account DELETE: removed {deletedAccount.Id}"); }
else DebugLogger.Log($"[Realtime] Account DELETE: id {deletedAccount.Id} not found (already removed locally)");
});
});
// Budgets
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedBudget = c.Model<Budget>();
if (insertedBudget is null) { DebugLogger.Log("[Realtime] Budget INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Budget INSERT: {insertedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
if (Budgets.Any(x => x.Id == insertedBudget.Id)) { DebugLogger.Log($"[Realtime] Budget INSERT: skipped duplicate {insertedBudget.Id}"); return; }
Budgets.Add(insertedBudget);
DebugLogger.Log($"[Realtime] Budget INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedBudget = c.Model<Budget>();
if (updatedBudget is null) { DebugLogger.Log("[Realtime] Budget UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Budget UPDATE: {updatedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
var idx = Budgets.ToList().FindIndex(x => x.Id == updatedBudget.Id);
if (idx != -1) { Budgets[idx] = updatedBudget; DebugLogger.Log($"[Realtime] Budget UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Budget UPDATE: id {updatedBudget.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Budget>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedBudget = c.OldModel<Budget>();
if (deletedBudget is null) { DebugLogger.Log("[Realtime] Budget DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Budget DELETE: {deletedBudget.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Budgets.FirstOrDefault(x => x.Id == deletedBudget.Id);
if (item is not null) { Budgets.Remove(item); DebugLogger.Log($"[Realtime] Budget DELETE: removed {deletedBudget.Id}"); }
else DebugLogger.Log($"[Realtime] Budget DELETE: id {deletedBudget.Id} not found (already removed locally)");
});
});
// Categories
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Inserts, (_, c) =>
{
var insertedCategory = c.Model<Category>();
if (insertedCategory is null) { DebugLogger.Log("[Realtime] Category INSERT: model was null"); return; }
DebugLogger.Log($"[Realtime] Category INSERT: {insertedCategory.Id} ({insertedCategory.Name})");
Dispatcher.UIThread.Post(() =>
{
if (Categories.Any(x => x.Id == insertedCategory.Id)) { DebugLogger.Log($"[Realtime] Category INSERT: skipped duplicate {insertedCategory.Id}"); return; }
Categories.Add(insertedCategory);
DebugLogger.Log($"[Realtime] Category INSERT: added to collection");
});
});
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var UpdatedCategory = c.Model<Category>();
if (UpdatedCategory is null) { DebugLogger.Log("[Realtime] Category UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Category UPDATE: {UpdatedCategory.Id} ({UpdatedCategory.Name})");
Dispatcher.UIThread.Post(() =>
{
var idx = Categories.ToList().FindIndex(x => x.Id == UpdatedCategory.Id);
if (idx != -1) { Categories[idx] = UpdatedCategory; DebugLogger.Log($"[Realtime] Category UPDATE: replaced at index {idx}"); }
else DebugLogger.Log($"[Realtime] Category UPDATE: id {UpdatedCategory.Id} not found in collection");
});
});
_ = SupabaseService.Client.From<Category>().On(PostgresChangesOptions.ListenType.Deletes, (_, c) =>
{
var deletedCategory = c.OldModel<Category>();
if (deletedCategory is null) { DebugLogger.Log("[Realtime] Category DELETE: old model was null"); return; }
DebugLogger.Log($"[Realtime] Category DELETE: {deletedCategory.Id}");
Dispatcher.UIThread.Post(() =>
{
var item = Categories.FirstOrDefault(x => x.Id == deletedCategory.Id);
if (item is not null) { Categories.Remove(item); DebugLogger.Log($"[Realtime] Category DELETE: removed {deletedCategory.Id}"); }
else DebugLogger.Log($"[Realtime] Category DELETE: id {deletedCategory.Id} not found (already removed locally)");
});
});
// Profile
_ = SupabaseService.Client.From<Profile>().On(PostgresChangesOptions.ListenType.Updates, (_, c) =>
{
var updatedProfile = c.Model<Profile>();
if (updatedProfile is null) { DebugLogger.Log("[Realtime] Profile UPDATE: model was null"); return; }
DebugLogger.Log($"[Realtime] Profile UPDATE: {updatedProfile.Id} ({updatedProfile.DisplayName})");
Dispatcher.UIThread.Post(() => Profile = updatedProfile);
});
DebugLogger.Log("[Realtime] all listeners registered");
}
}

14
Clario/Enums/AuthError.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace Clario.Enums;
public enum AuthError
{
InvalidCredentials,
EmailAlreadyExists,
EmailNotConfirmed,
WeakPassword,
InvalidEmail,
SignupDisabled,
RateLimited,
SessionExpired,
Unknown
}

View File

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

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