commit 97cafcfa1881d9141e2270e0ccc5793d047f5620 Author: Acrux Development Date: Sat Sep 19 12:38:12 2026 -0300 Add project files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4af7187 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +.vs/ +.idea/ +*.user diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..47de0b6 --- /dev/null +++ b/App.axaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..11f17d7 --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using NexvaneHub.ViewModels; +using NexvaneHub.Views; + +namespace NexvaneHub; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/Assets/nexvane-logo.svg b/Assets/nexvane-logo.svg new file mode 100644 index 0000000..94f1022 --- /dev/null +++ b/Assets/nexvane-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/Converters/EnumEqualsConverter.cs b/Converters/EnumEqualsConverter.cs new file mode 100644 index 0000000..9d58922 --- /dev/null +++ b/Converters/EnumEqualsConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace NexvaneHub.Converters; + +/// +/// Compares a bound enum value's string representation against ConverterParameter. +/// Used for "is this nav item selected" / "is this section visible" bindings. +/// +public class EnumEqualsConverter : IValueConverter +{ + public static readonly EnumEqualsConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null || parameter is null) + { + return false; + } + + return string.Equals(value.ToString(), parameter.ToString(), StringComparison.OrdinalIgnoreCase); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Models/ProjectModel.cs b/Models/ProjectModel.cs new file mode 100644 index 0000000..8782fbb --- /dev/null +++ b/Models/ProjectModel.cs @@ -0,0 +1,28 @@ +using System; + +namespace NexvaneHub.Models; + +/// +/// Plain data describing a project entry. Kept free of INotifyPropertyChanged +/// on purpose - ProjectItemViewModel wraps this for anything the UI binds to, +/// and writes edits back onto this instance. +/// +public class ProjectModel +{ + public string Name { get; set; } = string.Empty; + + public DateTime LastModified { get; set; } + + /// Not user-editable in the UI - tied to where the project lives on disk. + public string Directory { get; set; } = string.Empty; + + public string ThumbnailPath { get; set; } = string.Empty; + + public string ProjectVersion { get; set; } = "1.0.0"; + + public string SdkVersion { get; set; } = "1.4.2"; + + public string DefaultCompiler { get; set; } = "MSVC"; + + public string DefaultArchitecture { get; set; } = "Auto"; +} diff --git a/NexvaneHub.csproj b/NexvaneHub.csproj new file mode 100644 index 0000000..b2b9ca0 --- /dev/null +++ b/NexvaneHub.csproj @@ -0,0 +1,26 @@ + + + + WinExe + net10.0 + enable + true + true + NexvaneHub + NexvaneHub + + + + + + + + + + + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f09840b --- /dev/null +++ b/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace NexvaneHub; + +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); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e73317 --- /dev/null +++ b/README.md @@ -0,0 +1,314 @@ +# Nexvane Hub + +An Avalonia UI (MVVM) scaffold for a project-launcher "hub" app, styled to match +the Nexvane editor's dark UI and the cards/buttons from the marketing site. + +## Running it + +Requires the .NET 8 SDK. + +``` +dotnet restore +dotnet run +``` + +## Structure + +``` +NexvaneHub/ +├─ Models/ +│ └─ ProjectModel.cs Plain data: name, directory, SDK version, etc. +├─ ViewModels/ +│ ├─ ViewModelBase.cs +│ ├─ MainWindowViewModel.cs Nav state, project list, Open/Delete/New commands +│ └─ ProjectItemViewModel.cs +├─ Views/ +│ └─ MainWindow.axaml(.cs) Top bar + sidebar nav + card list + detail panel +├─ Converters/ +│ └─ EnumEqualsConverter.cs Drives nav selection & section switching +├─ Styles/ +│ ├─ Theme.axaml Color palette (brushes) — edit this to re-theme +│ └─ Controls.axaml Card / button / nav-item / field styles +└─ App.axaml(.cs) +``` + +## What's wired up vs. stubbed + +- The **Projects** section is fully built: card list, selection, detail panel + with collapsible "Project" / "SDK" groups, New / Open / Delete. +- Every field in the detail panel is a live, two-way-bound `TextBox` — + **Name** (the title), **Thumbnail**, **Version**, **SDK Version**, + **Default Compiler**, **Default Arch** — and edits write straight back onto + the underlying `ProjectModel` via the `OnChanged` partial methods in + `ProjectItemViewModel.cs`. **Directory** is the one exception: it's a plain + read-only field (not a `TextBox`), since it's tied to where the project + actually lives on disk rather than something you'd retype by hand. +- Default Compiler / Default Arch are plain text inputs here rather than + dropdowns of preset values (MSVC/Clang/GCC, x64/ARM64/Auto) — say the word + and I'll swap them for `ComboBox`es with those options. +- **SDK** and **Packages** nav items switch the middle pane but are placeholder + content — swap the placeholder `Border` in `MainWindow.axaml` for real views + when you're ready (a `SdkView.axaml` / `PackagesView.axaml` user control each, + same pattern as the project list). +- `OpenProjectCommand` and `DeleteProjectCommand` are stubbed in + `MainWindowViewModel` — `DeleteProject` already removes it from the in-memory + list, but neither touches disk. Hook `OpenProject` up to launching your editor + process against `SelectedProject.Directory`, and add a confirmation dialog to + `DeleteProject` before it goes near real files. +- Project thumbnails are plain placeholder tiles (no image loading yet) since + there's no asset pipeline in this scaffold — swap `Border.thumb` / + `Border.thumbLarge` for an `Image` bound to `ThumbnailPath` once you have + real screenshots to show. + +## Custom title bar + +The window uses a fully custom title bar (`ExtendClientAreaToDecorationsHint` ++ `ExtendClientAreaChromeHints="NoChrome"` in `MainWindow.axaml`), so the OS's +native title/caption buttons are gone and replaced with themed ones. Two +things make this work, both in `MainWindow.axaml.cs`: + +- Dragging: with no native chrome, the OS no longer treats the top bar as + draggable, so `TitleBar_PointerPressed` calls `BeginMoveDrag` by hand. The + settings gear and the minimize/maximize/close buttons sit inside that same + bar but consume their own `PointerPressed` before it bubbles up, so + clicking them doesn't also start a drag - you don't need to carve out + separate "drag zones" around them. +- Minimize/maximize/close: plain `Click` handlers that flip `WindowState` + or call `Close()`. + +This is the standard cross-platform-friendly approach (as opposed to +`SystemDecorations="None"`, which also removes OS-level resize/snap +behavior) but it's only been checked against the Windows screenshots you +sent — Linux/macOS may need small adjustments (e.g. macOS traffic-light +buttons behave differently under this hint). + +## Design system + +- **Rounded corners (6-8px) almost everywhere** — buttons, fields, cards, the + project list frame, the logo mark. The one exception is the **left sidebar + buttons** (Projects/SDK/Packages/Account), which stay square and flush + against each other, no radius, no margin — everything else softened. +- **Flush pairs where it matters:** sidebar nav items sit directly against + each other, and Open/Delete are one visual pair with no gap between them — + Open's own bottom border doubles as the divider, and only the outer + corners of the pair are rounded (`CornerRadius="6,6,0,0"` / `"0,0,6,6"`). +- **Accent blue appears only at the instant of interaction** — a button + being pressed, or a field gaining focus. Nothing stays accent-colored at + rest: the active sidebar tab is a neutral highlight + bold text, and the + Project/SDK section headers are neutral even while expanded. That second + one needed an explicit override — Avalonia's `ToggleButton` has a + built-in accent-colored `:checked` style from the Fluent theme, so + `Controls.axaml` overrides `ToggleButton.sectionHeader:checked` directly; + without that override the section headers would stay blue whenever + expanded regardless of anything else in the stylesheet. Delete keeps its + own red language on press rather than switching to blue, since a + destructive action turning "accent blue" would read as reassuring rather + than a warning. +- Palette pulled tighter to the editor screenshot: `#1B1B1D` background, + `#1F1F22` panels, `#2F6FED` accent. All of it lives in `Styles/Theme.axaml` + as named brushes — change values there and it propagates everywhere. Shapes + (radius, padding, borders) live in `Styles/Controls.axaml`. + +## Layout fixes from the last round + +- **Scrollbar gutter:** both scrollable areas (the project list, the detail + panel's fields) now have `Padding="0,0,14,0"` on their `ScrollViewer`, so + the scrollbar has its own space instead of drawing over the last ~14px of + content. +- **Divider vs. hover width:** `ListBoxItem` padding was moved off the item + itself and onto the content inside each row's `DataTemplate`. Previously + the item's own padding shrank the divider line but not the hover/selected + background, so the two didn't line up — now both span the row's full + width. +- **New folded into the Projects card:** the project list and the "New" + button now live inside one `Border` (rounded + `ClipToBounds="True"`), + with New as a bottom row separated only by a hairline — not a second, + separately-bordered element below the card. +- **Open/Delete got their divider back:** the fully-seamless version from + last round read as one blurred shape rather than two buttons. They're + still a single rounded, clipped block with zero margin between them, just + with a 1px hairline down the middle so the pairing is unambiguous. +- **Detail panel now belongs to the Projects tab:** the right column + (`Border.detailPanel`) is `IsVisible`-bound to `CurrentSection == Projects` + the same way the project list already was — it no longer stays on screen + showing stale project data while browsing SDK/Packages. Note the column + itself is a fixed `360` width in the outer `Grid.ColumnDefinitions`, so + when it's hidden that space goes blank rather than being reclaimed by the + middle column — say the word if you'd rather it collapse to `0` and let + the list expand to fill it. +- **Title bar menu row:** a second row (`File / Edit / View / Tools / Help`, + plain text buttons, no persistent fill) now sits below the title, per your + correction on the reference screenshot. Both rows live inside the same + draggable `Border.topBar`, so dragging still works from either row's blank + space, and the window's outer `Grid` row switched from a fixed `56` to + `Auto` to fit the taller two-row bar. + +## Menu dropdowns + +Each menu item opens a dropdown via `Button.Flyout` — Avalonia's built-in +mechanism handles positioning, click-away-to-close, and Escape-to-dismiss; +everything visible inside is our own styled content (`Button.dropdownItem` +rows + `Border.dropdownSeparator` dividers), not the default theme's `Menu`/ +`MenuItem` look. This was a deliberate choice: `MenuItem`'s built-in submenu +popup pulls its background/border from deeper theme resources than a plain +`Button`, so styling `Button` + `Flyout` (only two things to override: +`FlyoutPresenter` for the panel chrome, `Button.dropdownItem` for the rows) +was the more reliable path without being able to compile-check the result. + +- **Sections** are just `Border.dropdownSeparator` — a 1px divider — dropped + between groups of `Button.dropdownItem` rows in the `StackPanel`. Add or + remove them freely; there's no hidden logic tying them to anything. +- Every dropdown item is a placeholder with no `Click` handler, same as the + menu bar buttons themselves last round — clicking one closes the dropdown + (that part's free, from the Flyout) but doesn't do anything yet. Wiring + one up is a `Click="SomeHandler"` in the XAML plus a method in + `MainWindow.axaml.cs` (or a `Command` binding if it belongs on + `MainWindowViewModel` instead — e.g. "Toggle Sidebar" is a real candidate + for that once the sidebar is collapsible). +- The item lists (New Project, Undo, Toggle Sidebar, etc.) are my best guess + at what a project-hub menu would plausibly contain, not something you + asked for by name — trim, rename, or reorganize the sections however + actually fits. + +## Logo mark + +The title bar's logo badge now renders your actual `nexvane-logo.svg` +(kept in `Assets/nexvane-logo.svg` for reference) instead of a placeholder +"N" glyph, and spans the full height of both title-bar rows rather than +being confined to the top one — like the editor's title bar. Both were part +of the same request. + +- **Why it's inline vector, not an ``:** Avalonia has no built-in SVG + support, and pulling in an SVG-rendering package would mean a new + dependency I couldn't verify actually resolves/compiles without a working + `dotnet restore` here. Instead, the exact path data from your SVG (all + three `` elements, copied verbatim — the geometry itself isn't + something I redrew or approximated) is embedded directly as Avalonia + `Path` elements inside `MainWindow.axaml`, wrapped in a `Canvas` with the + same `scale(0.1,-0.1)` + `translate` your SVG applies, further shifted so + the mark's tight bounding box starts at `(0,0)` and a `Viewbox` scales it + cleanly to size. Net effect: what renders is your logo's real geometry, + using only built-in Avalonia controls — no new package, so nothing new + that can fail to restore. +- **The tall badge:** `Border.logoBadge` changed from a fixed `30x30` square + to a `64`-wide column that stretches to fill its `Grid` cell's height — + paired with moving it into its own `Grid.Column="0"` next to a + `Grid.Column="1"` holding both the title row and the menu row stacked + on top of each other, so the badge naturally spans both. `Border.topBar`'s + own padding moved from the whole bar onto just that second column + (`Margin="16,0,20,0"`), since the badge needs to sit flush in the corner, + not inset. +- If you'd rather use the actual `.svg` file directly (say, for reuse + elsewhere as an app icon) rather than this inlined copy, that's where + a package like `Avalonia.Svg.Skia` would come in — just flag it and I'll + wire it up, now that there's a known-working fallback already in place if + that package route hits a snag. + +## Title bar height fix + +The oversized bar (and the stray unstyled "Close" button floating below the +real controls) both trace back to the same thing: the bar's total height was +being **auto-measured** through several nested layers — `Viewbox` inside +`Border` inside `Grid` inside `StackPanel` inside `Border.topBar` — while +`ExtendClientAreaTitleBarHeightHint="-1"` told the OS to *auto-detect* that +same height from the content. Two auto-measurements that both depend on +several nested layers agreeing exactly are exactly the kind of thing that's +fragile to get wrong, and apparently did here. + +Fixed by pinning the number everywhere instead of deriving it: + +- `ExtendClientAreaTitleBarHeightHint="88"` — explicit, not `-1`. +- The window's outer `Grid.RowDefinitions` is `"88,*"`, not `"Auto,*"`. +- `Border.topBar` has an explicit `Height="88"` in `Controls.axaml`. +- The menu row is `Height="32"` directly, not implied by margin/padding. + +56 (title row) + 32 (menu row) = 88 everywhere, top to bottom, instead of +being something four different elements had to happen to agree on through +measurement. I couldn't run the app to confirm this eliminates the stray +"Close" button specifically — that part reads as Windows/Avalonia injecting +a fallback once the declared and auto-detected heights disagreed, which is +a reasonable explanation but not one I can verify without a build. If it's +still there after this, the next thing to try is `SystemDecorations="None"` +instead of the `ExtendClientArea*` hints — a blunter tool that drops native +resize/snap behavior, but leaves nothing for the OS to guess about. + +## Logo crop + menu alignment fix + +Two more from the same area, addressed separately since the title bar +height/Close-button issue above is parked for now: + +- **Logo was cropping.** The previous version scaled/positioned the mark via + `Canvas.RenderTransform` (a `ScaleTransform` + `TranslateTransform`) while + leaving the raw SVG coordinates untouched, relying on `Viewbox` to + correctly reconcile the `Canvas`'s *declared* size against a *separately + transformed* child. I re-verified the bounding-box math itself was + correct (recomputed it a second way, using a full path parser rather than + just tracking points, and got the same 800×899 result) — so the transform + numbers weren't wrong, but I couldn't rule out the reconciliation itself + as the failure point without being able to render it. Removed that + uncertainty entirely: the SVG's `scale(0.1,-0.1)` + `translate` is now + baked directly into every coordinate (computed once, offline), so each + `Path.Data` is already in its final coordinate space — no + `RenderTransform` left for `Viewbox` to reconcile against. There's also a + 2% safety margin baked into the `Canvas` size now, so even a small + rounding difference can't clip anything. +- **Menu row wasn't aligned with the title.** `Button.menuBarItem` has + `Padding="7,3"` for a comfortable hit target, but the title `TextBlock` + above it has no padding — so "File" rendered 7px right of where "Nexvane" + starts. Fixed with `Margin="-7,0,0,0"` on the menu row's `StackPanel`, + canceling that offset out so the two rows' text lines up. + +## Compacting the title bar + +The 88px bar had both rows *centered inside* fixed heights bigger than +their actual content needed, which read as loose/padded rather than tight +like the editor reference. Shrunk everything to hug its content instead of +floating inside oversized rows, and brought the bar's total height down to +match: + +- Title row: `56px → 38px`. Menu row: `32px → 24px`. Total: `88px → 62px` + (same pattern as before — pinned as an explicit number in four places: + `ExtendClientAreaTitleBarHeightHint`, the window's outer `Grid` row, + `Border.topBar`'s `Height`, and the two rows' own heights summing to it). +- Caption buttons (`38×26`, down from `44×32`), the settings gear + (`26×26`, down from `34×34`), and the logo `Viewbox` (`22×24.6`, down + from `26×29`) all shrank to match, so nothing looks oversized next to the + tighter text. +- `Button.menuBarItem`'s padding went from `7,3` to `6,2`, and the + alignment-compensating margin on the menu row updated from `-7` to `-6` + to match. +- `Border.logoBadge` narrowed from `64px` to `56px` wide, in proportion + with the shorter bar. + +All of this is proportional scaling from the same values as before, not a +redesign — if 62px is still taller (or shorter) than you'd like, every +number above is a single line in `Controls.axaml` or `MainWindow.axaml`, +not something that requires touching the structure. + +## Fixing the logo/text vertical balance + +Two related issues after compacting: the text rows visually overshooting +the logo (top and bottom), and everything sitting too close to the bar's +edges. Same root cause for both — the logo's `Viewbox` (`22×24.6`) was +small relative to its `62px`-tall column, so it rendered as a small mark +centered in a lot of invisible padding, while the title/menu rows spanned +the *full* column height with only their own font-metric spacing. The +logo's visible ink and the text's visible extent weren't bounded by the +same space, even though they shared the same container. + +Fixed by giving both the same physical space to live in: + +- `Border.topBar` now has `Padding="0,3,0,3"` — a shared 3px inset from the + bar's top and bottom that both the logo *and* the text column sit inside + (it's on the shared parent, not applied separately to each side, so + there's no way for them to drift out of sync again). Total height is + still `62px`; the title/menu rows shrank from `38/24` to `34/22` to fit + inside the now-smaller `56px` content area (`34+22=56`, `+3+3` padding + `=62`). +- The logo `Viewbox` grew from `22×24.6` to `43×48` — closer to filling + that `56px` content height instead of floating small in the middle, so + its visible bounds now roughly bracket the text block instead of being + dwarfed by it. `Border.logoBadge` widened from `56px` to `60px` to give + the bigger mark reasonable side clearance. +- The text column's margins grew from `14,0,16,0` to `20,0,20,0` for more + breathing room from both the logo and the window's right edge. diff --git a/Styles/Controls.axaml b/Styles/Controls.axaml new file mode 100644 index 0000000..26c8029 --- /dev/null +++ b/Styles/Controls.axaml @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Styles/Theme.axaml b/Styles/Theme.axaml new file mode 100644 index 0000000..a51bc1e --- /dev/null +++ b/Styles/Theme.axaml @@ -0,0 +1,60 @@ + + + + + + + #1B1B1D + #1F1F22 + #18181A + #222225 + #2A2A2E + #333338 + #2C2C30 + #2A2A2E + #2F6FED + #E5484D + #C53A3F + #F2F2F3 + #9A9AA0 + #6C6C72 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..7b27523 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using NexvaneHub.Models; + +namespace NexvaneHub.ViewModels; + +public enum HubSection +{ + Projects, + Sdk, + Packages, +} + +public partial class MainWindowViewModel : ViewModelBase +{ + public MainWindowViewModel() + { + _projects = new ObservableCollection + { + new(new ProjectModel + { + Name = "Project A", + LastModified = new DateTime(2026, 9, 6), + Directory = @"C:\Dev\ProjectA", + ProjectVersion = "1.2.0", + SdkVersion = "1.4.2", + DefaultCompiler = "MSVC", + DefaultArchitecture = "Auto", + }), + new(new ProjectModel + { + Name = "Project B", + LastModified = new DateTime(2026, 8, 2), + Directory = @"C:\Dev\ProjectB", + ProjectVersion = "0.9.4", + SdkVersion = "1.3.0", + DefaultCompiler = "Clang", + DefaultArchitecture = "x64", + }), + new(new ProjectModel + { + Name = "Project C", + LastModified = new DateTime(2026, 6, 18), + Directory = @"C:\Dev\ProjectC", + ProjectVersion = "1.0.0", + SdkVersion = "1.4.2", + DefaultCompiler = "MSVC", + DefaultArchitecture = "Auto", + }), + new(new ProjectModel + { + Name = "Project D", + LastModified = new DateTime(2026, 5, 30), + Directory = @"C:\Dev\ProjectD", + ProjectVersion = "0.4.1", + SdkVersion = "1.2.7", + DefaultCompiler = "GCC", + DefaultArchitecture = "ARM64", + }), + }; + + _selectedProject = _projects.Count > 0 ? _projects[0] : null; + } + + [ObservableProperty] + private HubSection _currentSection = HubSection.Projects; + + [ObservableProperty] + private ObservableCollection _projects; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(OpenProjectCommand))] + [NotifyCanExecuteChangedFor(nameof(DeleteProjectCommand))] + private ProjectItemViewModel? _selectedProject; + + [ObservableProperty] + private bool _isProjectSectionExpanded = true; + + [ObservableProperty] + private bool _isSdkSectionExpanded = true; + + [RelayCommand] + private void NavigateTo(HubSection section) + { + CurrentSection = section; + } + + [RelayCommand(CanExecute = nameof(HasSelectedProject))] + private void OpenProject() + { + // TODO: hook this up to actually launching the editor process + // against SelectedProject.Directory. + } + + [RelayCommand(CanExecute = nameof(HasSelectedProject))] + private void DeleteProject() + { + if (SelectedProject is null) + { + return; + } + + var index = Projects.IndexOf(SelectedProject); + Projects.Remove(SelectedProject); + + SelectedProject = Projects.Count == 0 + ? null + : Projects[Math.Min(index, Projects.Count - 1)]; + } + + private bool HasSelectedProject() => SelectedProject is not null; + + [RelayCommand] + private void CreateNewProject() + { + var project = new ProjectItemViewModel(new ProjectModel + { + Name = $"New Project {Projects.Count + 1}", + LastModified = DateTime.Now, + Directory = @"C:\Dev\NewProject", + }); + + Projects.Insert(0, project); + SelectedProject = project; + } +} diff --git a/ViewModels/ProjectItemViewModel.cs b/ViewModels/ProjectItemViewModel.cs new file mode 100644 index 0000000..f092635 --- /dev/null +++ b/ViewModels/ProjectItemViewModel.cs @@ -0,0 +1,65 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using NexvaneHub.Models; + +namespace NexvaneHub.ViewModels; + +/// +/// Editable wrapper around a ProjectModel. Every property here is a live, +/// bindable field that writes straight back onto the wrapped Model when +/// changed - except Directory, which stays read-only since it's tied to the +/// project's location on disk rather than something you'd retype. +/// +public partial class ProjectItemViewModel : ViewModelBase +{ + public ProjectItemViewModel(ProjectModel model) + { + Model = model; + + _name = model.Name; + _lastModified = model.LastModified; + _directory = model.Directory; + _thumbnailPath = model.ThumbnailPath; + _projectVersion = model.ProjectVersion; + _sdkVersion = model.SdkVersion; + _defaultCompiler = model.DefaultCompiler; + _defaultArchitecture = model.DefaultArchitecture; + } + + public ProjectModel Model { get; } + + public string LastModifiedDisplay => LastModified.ToString("dd-MM-yyyy"); + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(LastModifiedDisplay))] + private DateTime _lastModified; + + [ObservableProperty] + private string _name; + + /// Read-only in the UI - displayed via a plain field, not a TextBox. + [ObservableProperty] + private string _directory; + + [ObservableProperty] + private string _thumbnailPath; + + [ObservableProperty] + private string _projectVersion; + + [ObservableProperty] + private string _sdkVersion; + + [ObservableProperty] + private string _defaultCompiler; + + [ObservableProperty] + private string _defaultArchitecture; + + partial void OnNameChanged(string value) => Model.Name = value; + partial void OnThumbnailPathChanged(string value) => Model.ThumbnailPath = value; + partial void OnProjectVersionChanged(string value) => Model.ProjectVersion = value; + partial void OnSdkVersionChanged(string value) => Model.SdkVersion = value; + partial void OnDefaultCompilerChanged(string value) => Model.DefaultCompiler = value; + partial void OnDefaultArchitectureChanged(string value) => Model.DefaultArchitecture = value; +} diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..f253e50 --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace NexvaneHub.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml new file mode 100644 index 0000000..e25f623 --- /dev/null +++ b/Views/MainWindow.axaml @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +