Skip to content

Commit 8879191

Browse files
committed
redo of IconUrl and ImageSource
1 parent 0fefc8c commit 8879191

File tree

3 files changed

+103
-74
lines changed

3 files changed

+103
-74
lines changed

App/ViewModels/AgentAppViewModel.cs

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
11
using System;
22
using System.Linq;
33
using Windows.System;
4+
using Coder.Desktop.App.Models;
5+
using Coder.Desktop.App.Services;
46
using Coder.Desktop.App.Utils;
57
using Coder.Desktop.Vpn.Proto;
68
using CommunityToolkit.Mvvm.ComponentModel;
79
using CommunityToolkit.Mvvm.Input;
810
using Microsoft.Extensions.Logging;
911
using Microsoft.UI.Xaml;
12+
using Microsoft.UI.Xaml.Controls;
13+
using Microsoft.UI.Xaml.Controls.Primitives;
1014
using Microsoft.UI.Xaml.Media;
1115
using Microsoft.UI.Xaml.Media.Imaging;
1216

1317
namespace Coder.Desktop.App.ViewModels;
1418

1519
public interface IAgentAppViewModelFactory
1620
{
17-
public AgentAppViewModel Create(Uuid id, string name, Uri appUri, Uri? iconUrl);
21+
public AgentAppViewModel Create(Uuid id, string name, string appUri, Uri? iconUrl);
1822
}
1923

20-
public class AgentAppViewModelFactory : IAgentAppViewModelFactory
24+
public class AgentAppViewModelFactory(ILogger<AgentAppViewModel> childLogger, ICredentialManager credentialManager)
25+
: IAgentAppViewModelFactory
2126
{
22-
private readonly ILogger<AgentAppViewModel> _childLogger;
23-
24-
public AgentAppViewModelFactory(ILogger<AgentAppViewModel> childLogger)
25-
{
26-
_childLogger = childLogger;
27-
}
28-
29-
public AgentAppViewModel Create(Uuid id, string name, Uri appUri, Uri? iconUrl)
27+
public AgentAppViewModel Create(Uuid id, string name, string appUri, Uri? iconUrl)
3028
{
31-
return new AgentAppViewModel(_childLogger)
29+
return new AgentAppViewModel(childLogger, credentialManager)
3230
{
3331
Id = id,
3432
Name = name,
@@ -40,61 +38,40 @@ public AgentAppViewModel Create(Uuid id, string name, Uri appUri, Uri? iconUrl)
4038

4139
public partial class AgentAppViewModel : ObservableObject, IModelUpdateable<AgentAppViewModel>
4240
{
41+
private const string SessionTokenUriVar = "$SESSION_TOKEN";
42+
4343
private readonly ILogger<AgentAppViewModel> _logger;
44+
private readonly ICredentialManager _credentialManager;
4445

4546
public required Uuid Id { get; init; }
4647

4748
[ObservableProperty] public required partial string Name { get; set; }
4849

4950
[ObservableProperty]
5051
[NotifyPropertyChangedFor(nameof(Details))]
51-
public required partial Uri AppUri { get; set; }
52+
public required partial string AppUri { get; set; }
5253

53-
[ObservableProperty]
54-
[NotifyPropertyChangedFor(nameof(ImageSource))]
55-
public partial Uri? IconUrl { get; set; }
54+
[ObservableProperty] public partial Uri? IconUrl { get; set; }
55+
56+
[ObservableProperty] public partial ImageSource IconImageSource { get; set; }
5657

5758
[ObservableProperty] public partial bool UseFallbackIcon { get; set; } = true;
5859

5960
public string Details =>
6061
(string.IsNullOrWhiteSpace(Name) ? "(no name)" : Name) + ":\n\n" + AppUri;
6162

62-
public ImageSource ImageSource
63-
{
64-
get
65-
{
66-
if (IconUrl is null || (IconUrl.Scheme != "http" && IconUrl.Scheme != "https"))
67-
{
68-
UseFallbackIcon = true;
69-
return new BitmapImage();
70-
}
71-
72-
// Determine what image source to use based on extension, use a
73-
// BitmapImage as last resort.
74-
var ext = IconUrl.AbsolutePath.Split('/').LastOrDefault()?.Split('.').LastOrDefault();
75-
// TODO: this is definitely a hack, URLs shouldn't need to end in .svg
76-
if (ext is "svg")
77-
{
78-
// TODO: Some SVGs like `/icon/cursor.svg` contain PNG data and
79-
// don't render at all.
80-
var svg = new SvgImageSource(IconUrl);
81-
svg.Opened += (_, _) => _logger.LogDebug("app icon opened (svg): {uri}", IconUrl);
82-
svg.OpenFailed += (_, args) =>
83-
_logger.LogDebug("app icon failed to open (svg): {uri}: {Status}", IconUrl, args.Status);
84-
return svg;
85-
}
86-
87-
var bitmap = new BitmapImage(IconUrl);
88-
bitmap.ImageOpened += (_, _) => _logger.LogDebug("app icon opened (bitmap): {uri}", IconUrl);
89-
bitmap.ImageFailed += (_, args) =>
90-
_logger.LogDebug("app icon failed to open (bitmap): {uri}: {ErrorMessage}", IconUrl, args.ErrorMessage);
91-
return bitmap;
92-
}
93-
}
94-
95-
public AgentAppViewModel(ILogger<AgentAppViewModel> logger)
63+
public AgentAppViewModel(ILogger<AgentAppViewModel> logger, ICredentialManager credentialManager)
9664
{
9765
_logger = logger;
66+
_credentialManager = credentialManager;
67+
68+
// Apply the icon URL to the icon image source when it is updated.
69+
IconImageSource = UpdateIcon();
70+
PropertyChanged += (_, args) =>
71+
{
72+
if (args.PropertyName == nameof(IconUrl))
73+
IconImageSource = UpdateIcon();
74+
};
9875
}
9976

10077
public bool TryApplyChanges(AgentAppViewModel obj)
@@ -116,6 +93,36 @@ public bool TryApplyChanges(AgentAppViewModel obj)
11693
return true;
11794
}
11895

96+
private ImageSource UpdateIcon()
97+
{
98+
if (IconUrl is null || (IconUrl.Scheme != "http" && IconUrl.Scheme != "https"))
99+
{
100+
UseFallbackIcon = true;
101+
return new BitmapImage();
102+
}
103+
104+
// Determine what image source to use based on extension, use a
105+
// BitmapImage as last resort.
106+
var ext = IconUrl.AbsolutePath.Split('/').LastOrDefault()?.Split('.').LastOrDefault();
107+
// TODO: this is definitely a hack, URLs shouldn't need to end in .svg
108+
if (ext is "svg")
109+
{
110+
// TODO: Some SVGs like `/icon/cursor.svg` contain PNG data and
111+
// don't render at all.
112+
var svg = new SvgImageSource(IconUrl);
113+
svg.Opened += (_, _) => _logger.LogDebug("app icon opened (svg): {uri}", IconUrl);
114+
svg.OpenFailed += (_, args) =>
115+
_logger.LogDebug("app icon failed to open (svg): {uri}: {Status}", IconUrl, args.Status);
116+
return svg;
117+
}
118+
119+
var bitmap = new BitmapImage(IconUrl);
120+
bitmap.ImageOpened += (_, _) => _logger.LogDebug("app icon opened (bitmap): {uri}", IconUrl);
121+
bitmap.ImageFailed += (_, args) =>
122+
_logger.LogDebug("app icon failed to open (bitmap): {uri}: {ErrorMessage}", IconUrl, args.ErrorMessage);
123+
return bitmap;
124+
}
125+
119126
public void OnImageOpened(object? sender, RoutedEventArgs e)
120127
{
121128
UseFallbackIcon = false;
@@ -127,15 +134,45 @@ public void OnImageFailed(object? sender, RoutedEventArgs e)
127134
}
128135

129136
[RelayCommand]
130-
private void OpenApp()
137+
private void OpenApp(object parameter)
131138
{
132139
try
133140
{
134-
_ = Launcher.LaunchUriAsync(AppUri);
141+
var uriString = AppUri;
142+
var cred = _credentialManager.GetCachedCredentials();
143+
if (cred.State is CredentialState.Valid && cred.ApiToken is not null)
144+
uriString = uriString.Replace(SessionTokenUriVar, cred.ApiToken);
145+
uriString += SessionTokenUriVar;
146+
if (uriString.Contains(SessionTokenUriVar))
147+
throw new Exception($"URI contains {SessionTokenUriVar} variable but could not be replaced");
148+
149+
var uri = new Uri(uriString);
150+
_ = Launcher.LaunchUriAsync(uri);
135151
}
136-
catch
152+
catch (Exception e)
137153
{
138-
// TODO: log/notify
154+
_logger.LogWarning(e, "could not parse or launch app");
155+
156+
if (parameter is not FrameworkElement frameworkElement) return;
157+
var flyout = new Flyout
158+
{
159+
Content = new TextBlock
160+
{
161+
Text = $"Could not open app: {e.Message}",
162+
Margin = new Thickness(4),
163+
TextWrapping = TextWrapping.Wrap,
164+
},
165+
FlyoutPresenterStyle = new Style(typeof(FlyoutPresenter))
166+
{
167+
Setters =
168+
{
169+
new Setter(ScrollViewer.HorizontalScrollModeProperty, ScrollMode.Disabled),
170+
new Setter(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Disabled),
171+
},
172+
},
173+
};
174+
FlyoutBase.SetAttachedFlyout(frameworkElement, flyout);
175+
FlyoutBase.ShowAttachedFlyout(frameworkElement);
139176
}
140177
}
141178
}

App/ViewModels/AgentViewModel.cs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,16 @@ public AgentViewModel Create(Uuid id, string hostname, string hostnameSuffix,
2626
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName);
2727
}
2828

29-
public class AgentViewModelFactory : IAgentViewModelFactory
29+
public class AgentViewModelFactory(
30+
ILogger<AgentViewModel> childLogger,
31+
ICoderApiClientFactory coderApiClientFactory,
32+
ICredentialManager credentialManager,
33+
IAgentAppViewModelFactory agentAppViewModelFactory) : IAgentViewModelFactory
3034
{
31-
private readonly ILogger<AgentViewModel> _childLogger;
32-
private readonly ICoderApiClientFactory _coderApiClientFactory;
33-
private readonly ICredentialManager _credentialManager;
34-
private readonly IAgentAppViewModelFactory _agentAppViewModelFactory;
35-
36-
public AgentViewModelFactory(ILogger<AgentViewModel> childLogger, ICoderApiClientFactory coderApiClientFactory,
37-
ICredentialManager credentialManager, IAgentAppViewModelFactory agentAppViewModelFactory)
38-
{
39-
_childLogger = childLogger;
40-
_coderApiClientFactory = coderApiClientFactory;
41-
_credentialManager = credentialManager;
42-
_agentAppViewModelFactory = agentAppViewModelFactory;
43-
}
44-
4535
public AgentViewModel Create(Uuid id, string hostname, string hostnameSuffix,
4636
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName)
4737
{
48-
return new AgentViewModel(_childLogger, _coderApiClientFactory, _credentialManager, _agentAppViewModelFactory)
38+
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory)
4939
{
5040
Id = id,
5141
Hostname = hostname,
@@ -270,18 +260,18 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
270260
continue;
271261
}
272262

273-
if (!Uri.TryCreate(app.Url, UriKind.Absolute, out var appUri))
263+
if (string.IsNullOrEmpty(app.Url))
274264
{
275-
_logger.LogWarning("Could not parse app URI '{Url}' for '{DisplayName}', app will not appear in list",
276-
app.Url, app.DisplayName);
265+
_logger.LogWarning("App URI '{Url}' for '{DisplayName}' is empty, app will not appear in list", app.Url,
266+
app.DisplayName);
277267
continue;
278268
}
279269

280270
// Icon parse failures are not fatal, we will just use the fallback
281271
// icon.
282272
_ = Uri.TryCreate(DashboardBaseUrl, app.Icon, out var iconUrl);
283273

284-
apps.Add(_agentAppViewModelFactory.Create(uuid, app.DisplayName, appUri, iconUrl));
274+
apps.Add(_agentAppViewModelFactory.Create(uuid, app.DisplayName, app.Url, iconUrl));
285275
}
286276

287277
// Sort by name.

App/Views/Pages/TrayWindowMainPage.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,16 +251,18 @@
251251
<ItemsRepeater.ItemTemplate>
252252
<DataTemplate x:DataType="viewModels:AgentAppViewModel">
253253
<HyperlinkButton
254+
x:Name="AppButton"
254255
Padding="6"
255256
Margin="0"
256257
Command="{x:Bind OpenAppCommand}"
258+
CommandParameter="{Binding ElementName=AppButton}"
257259
Width="34"
258260
Height="34"
259261
ToolTipService.ToolTip="{x:Bind Details}">
260262

261263
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
262264
<Image
263-
Source="{x:Bind ImageSource}"
265+
Source="{x:Bind IconImageSource, Mode=OneWay}"
264266
ImageOpened="{x:Bind OnImageOpened}"
265267
ImageFailed="{x:Bind OnImageFailed}"
266268
Visibility="{x:Bind UseFallbackIcon, Converter={StaticResource InverseBoolToVisibilityConverter}, Mode=OneWay}"

0 commit comments

Comments
 (0)