11using System ;
22using System . Linq ;
33using Windows . System ;
4+ using Coder . Desktop . App . Models ;
5+ using Coder . Desktop . App . Services ;
46using Coder . Desktop . App . Utils ;
57using Coder . Desktop . Vpn . Proto ;
68using CommunityToolkit . Mvvm . ComponentModel ;
79using CommunityToolkit . Mvvm . Input ;
810using Microsoft . Extensions . Logging ;
911using Microsoft . UI . Xaml ;
12+ using Microsoft . UI . Xaml . Controls ;
13+ using Microsoft . UI . Xaml . Controls . Primitives ;
1014using Microsoft . UI . Xaml . Media ;
1115using Microsoft . UI . Xaml . Media . Imaging ;
1216
1317namespace Coder . Desktop . App . ViewModels ;
1418
1519public 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
4139public 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}
0 commit comments