Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,17 @@ jobs:
sed "s|@VERSION@|${VERSION}|g" \
scripts/macos/Info.plist \
> "${APP_BUNDLE}/Contents/Info.plist"
# Compile the appearance-aware app icon (Default/Dark/Tinted/Clear) from the Icon Composer
# .icon into the bundle: produces Assets.car + AppIcon.icns. Info.plist already declares
# CFBundleIconName=AppIcon, so macOS renders the appearance styles itself (incl. when closed).
xcrun actool scripts/macos/AppIcon.icon \
--compile "${APP_BUNDLE}/Contents/Resources" \
--app-icon AppIcon \
--output-partial-info-plist "$(mktemp).plist" \
--platform macosx \
--minimum-deployment-target 12.0 \
--errors --warnings
# Static .icns fallback for macOS < 26 (CFBundleIconFile) and the dmg volume icon.
cp scripts/macos/UniGetUI.icns "${APP_BUNDLE}/Contents/Resources/"

if [ "$DRY_RUN" = "false" ]; then
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions scripts/macos/AppIcon.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"fill" : {
"linear-gradient" : [
"display-p3:1.00000,0.99646,0.99994,1.00000",
"display-p3:0.96783,0.93622,0.94438,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5,
"y" : 0.7
}
}
},
"groups" : [
{
"layers" : [
{
"blend-mode" : "normal",
"glass" : false,
"hidden" : false,
"image-name" : "devolutions-unigetui-icon.svg",
"name" : "devolutions-unigetui-icon",
"position" : {
"scale" : 16,
"translation-in-points" : [
18.7109375,
-22.06000000000006
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
4 changes: 4 additions & 0 deletions scripts/macos/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<string>UniGetUI</string>
<key>CFBundleExecutable</key>
<string>UniGetUI.Avalonia</string>
<!-- CFBundleIconName (Assets.car, compiled from AppIcon.icon) drives the macOS 26 appearance
styling; CFBundleIconFile (UniGetUI.icns) is the static fallback for macOS < 26. -->
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIconFile</key>
<string>UniGetUI</string>
<key>CFBundlePackageType</key>
Expand Down
Binary file modified scripts/macos/UniGetUI.icns
Binary file not shown.
35 changes: 30 additions & 5 deletions src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
using Avalonia.Threading;
#if AVALONIA_DIAGNOSTICS_ENABLED
Expand All @@ -24,11 +26,34 @@ public partial class App : Application
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);

// Windows 11 Mica look is opt-in per environment: only merge the translucent
// surface overrides when Mica is actually usable (Win11 + transparency on).
// macOS, Linux, Windows 10, and transparency-off all keep the solid Styles.Common look.
if (MicaWindowHelper.IsMicaEnabled())
ApplyWindowsMicaStyling();
#if AVALONIA_DIAGNOSTICS_ENABLED
this.AttachDeveloperTools();
#endif
}

// ResourceInclude is flagged with RequiresUnreferencedCode because, in general, it can load
// resources from other assemblies that trimming might remove. Styles.WindowsMica.axaml is an
// avares resource embedded in THIS assembly, so it is never trimmed — the warning is safe to
// suppress here. (It can't be declared in XAML because the merge is conditional at runtime.)
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "Styles.WindowsMica.axaml is an avares resource in this assembly and is not trimmed.")]
private void ApplyWindowsMicaStyling()
{
Resources.MergedDictionaries.Add(new ResourceInclude((Uri?)null)
{
Source = new Uri("avares://UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml")
});
// Give flyouts/menus/tooltips a native acrylic backdrop (DWM) so they blur + tint
// from behind and adapt to the theme.
MicaWindowHelper.EnableAcrylicPopups();
}

public override void OnFrameworkInitializationCompleted()
{
if (OperatingSystem.IsWindows())
Expand Down Expand Up @@ -81,11 +106,11 @@ private static void StartMainWindow(IClassicDesktopStyleApplicationLifetime desk
{
if (OperatingSystem.IsMacOS())
{
// The Dock icon (incl. Default/Dark/Tinted/Clear styling) is provided by the .app bundle's
// AppIcon (scripts/macos/AppIcon.icon → Assets.car, via CFBundleIconName) and rendered by
// the system — for packaged releases and for Debug builds, which also build into a .app
// (see UniGetUI.Avalonia.csproj). There is nothing to do at runtime.
ProcessEnvironmentConfigurator.PrepareForCurrentPlatform();
using var stream = AssetLoader.Open(new Uri("avares://UniGetUI.Avalonia/Assets/icon.png"));
using var ms = new MemoryStream();
stream.CopyTo(ms);
MacOsNotificationBridge.SetDockIcon(ms.ToArray());
}
else
{
Expand Down
4 changes: 4 additions & 0 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<!-- Window structure -->
<SolidColorBrush x:Key="AppWindowBackground" Color="#F3F3F3"/>
<SolidColorBrush x:Key="AppTitleBarBackground" Color="#F3F3F3"/>
<!-- Window/page surface; overridden to Transparent by Styles.WindowsMica when Mica is active -->
<SolidColorBrush x:Key="MicaPageBackground" Color="#F3F3F3"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#E5E5E5"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#EEEEEE"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#E5E5E5"/>
Expand Down Expand Up @@ -97,6 +99,8 @@
<!-- Window structure -->
<SolidColorBrush x:Key="AppWindowBackground" Color="#202020"/>
<SolidColorBrush x:Key="AppTitleBarBackground" Color="#202020"/>
<!-- Window/page surface; overridden to Transparent by Styles.WindowsMica when Mica is active -->
<SolidColorBrush x:Key="MicaPageBackground" Color="#202020"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#1A1A1A"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#1C1C1C"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#353535"/>
Expand Down
89 changes: 89 additions & 0 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Windows 11 Mica overrides. Merged into Application.Resources ONLY when Mica is
actually active (Windows 11 + "Transparency effects" on) — see App.Initialize().
On macOS, Linux, Windows 10, or when transparency is off, this is never merged,
so the app keeps the solid surfaces defined in Styles.Common.axaml.

These keys make the window/page/dialog/card surfaces translucent so the Mica
backdrop shows through and the surfaces inherit its (wallpaper-derived) tint.
-->
<ResourceDictionary.ThemeDictionaries>

<ResourceDictionary x:Key="Light">
<!-- Window/page surface goes transparent so Mica shows behind chrome + page -->
<SolidColorBrush x:Key="MicaPageBackground" Color="Transparent"/>
<!-- Cards / panels: translucent white layers over the (light) Mica -->
<SolidColorBrush x:Key="SettingsCardBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="SettingsCardBorderBrush" Color="#14000000"/>
<SolidColorBrush x:Key="SettingsCardHoverBackground" Color="#99FFFFFF"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#18000000"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#18000000"/>
<SolidColorBrush x:Key="AppBadgeBackground" Color="#99FFFFFF"/>
<SolidColorBrush x:Key="AppDialogPanelBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppDialogSubtleBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppDialogDarkBackground" Color="#80FFFFFF"/>
<!-- Flyouts / menus / dropdowns / tooltips: DWM Mica backdrop with the card tint over it -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="ToolTipBackground" Color="#80FFFFFF"/>
<!-- Expander chrome -->
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPointerOver" Color="#99FFFFFF"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPressed" Color="#A6FFFFFF"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundDisabled" Color="#40FFFFFF"/>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#14000000"/>
<SolidColorBrush x:Key="ExpanderContentBorderBrush" Color="#14000000"/>
<!-- Tab headers (selected tab matches the content panel so they read as one surface) -->
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelected" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselected" Color="Transparent"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselectedPointerOver" Color="#0F000000"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelectedPointerOver" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselectedPressed" Color="#08000000"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelectedPressed" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundDisabled" Color="Transparent"/>
</ResourceDictionary>

<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="MicaPageBackground" Color="Transparent"/>
<!-- Cards / panels: translucent dark layers; ~40% so the wallpaper-tinted Mica bleeds through -->
<SolidColorBrush x:Key="SettingsCardBackground" Color="#662D2D2D"/>
<SolidColorBrush x:Key="SettingsCardBorderBrush" Color="#26FFFFFF"/>
<SolidColorBrush x:Key="SettingsCardHoverBackground" Color="#66383838"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#66000000"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#66202020"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#33FFFFFF"/>
<SolidColorBrush x:Key="AppBadgeBackground" Color="#66383838"/>
<SolidColorBrush x:Key="AppDialogPanelBackground" Color="#662C2C2C"/>
<SolidColorBrush x:Key="AppDialogSubtleBackground" Color="#66393939"/>
<SolidColorBrush x:Key="AppDialogDarkBackground" Color="#66181818"/>
<!-- Flyouts / menus / dropdowns / tooltips: MicaWindowHelper gives the popup window the same
DWM Mica backdrop the app uses; this tint matches the cards so menus read identically -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#662D2D2D"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#662D2D2D"/>
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#662D2D2D"/>
<SolidColorBrush x:Key="ToolTipBackground" Color="#662D2D2D"/>
<!-- Expander chrome -->
<SolidColorBrush x:Key="ExpanderHeaderBackground" Color="#662D2D2D"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPointerOver" Color="#802D2D2D"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundPressed" Color="#8C2D2D2D"/>
<SolidColorBrush x:Key="ExpanderHeaderBackgroundDisabled" Color="#402D2D2D"/>
<SolidColorBrush x:Key="ExpanderContentBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ExpanderHeaderBorderBrush" Color="#26FFFFFF"/>
<SolidColorBrush x:Key="ExpanderContentBorderBrush" Color="#26FFFFFF"/>
<!-- Tab headers -->
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelected" Color="#662C2C2C"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselected" Color="Transparent"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselectedPointerOver" Color="#1AFFFFFF"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelectedPointerOver" Color="#662C2C2C"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundUnselectedPressed" Color="#0FFFFFFF"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundSelectedPressed" Color="#662C2C2C"/>
<SolidColorBrush x:Key="TabItemHeaderBackgroundDisabled" Color="Transparent"/>
</ResourceDictionary>

</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
97 changes: 97 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/DirectionalSlideTransition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree;

namespace UniGetUI.Avalonia.Infrastructure;

/// <summary>
/// Horizontal page slide whose direction is set explicitly via <see cref="Reverse"/>.
/// (TransitioningContentControl always reports forward navigation, so the caller toggles this
/// before changing content.) Reverse=false slides the incoming page in from the right
/// (drill-in); Reverse=true slides it in from the left (back navigation).
/// Scrollbars are hidden for the duration so they don't drag across the view.
/// </summary>
public sealed class DirectionalSlideTransition : IPageTransition
{
public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(220);

public bool Reverse { get; set; }

public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
double sign = Reverse ? -1d : 1d;
double width = (to ?? from)?.GetVisualParent()?.Bounds.Width
?? (to ?? from)?.Bounds.Width ?? 0d;

var hidden = new List<ScrollViewer>();
HideScrollBars(from, hidden);
HideScrollBars(to, hidden);

try
{
var tasks = new List<Task>();
if (from is not null)
tasks.Add(Slide(from, 0d, -sign * width, cancellationToken));
if (to is not null)
tasks.Add(Slide(to, sign * width, 0d, cancellationToken));
await Task.WhenAll(tasks);
}
finally
{
foreach (var sv in hidden)
sv.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
}

if (cancellationToken.IsCancellationRequested)
return;

// Hide before clearing the transform so the outgoing page never snaps back on-screen.
if (from is not null)
{
from.IsVisible = false;
from.RenderTransform = null;
}
to?.RenderTransform = null;
}

private static void HideScrollBars(Visual? root, List<ScrollViewer> hidden)
{
if (root is null)
return;

foreach (var sv in root.GetVisualDescendants().OfType<ScrollViewer>())
{
if (sv.VerticalScrollBarVisibility == ScrollBarVisibility.Auto)
{
sv.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
hidden.Add(sv);
}
}
}

private Task Slide(Visual target, double fromX, double toX, CancellationToken cancellationToken)
{
var anim = new Animation
{
Duration = Duration,
Easing = new CubicEaseInOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(TranslateTransform.XProperty, fromX) } },
new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(TranslateTransform.XProperty, toX) } },
},
};
return anim.RunAsync(target, cancellationToken);
}
}
Loading
Loading