77using Windows . ApplicationModel ;
88using Windows . ApplicationModel . Activation ;
99using Windows . Storage ;
10+ using CommunityToolkit . Mvvm . Messaging ;
1011using H . NotifyIcon ;
1112using Microsoft . Extensions . DependencyInjection ;
1213using Microsoft . Extensions . Logging ;
1516using OwlCore . Storage ;
1617using SecureFolderFS . Sdk . AppModels ;
1718using SecureFolderFS . Sdk . DataModels ;
19+ using SecureFolderFS . Sdk . Messages ;
1820using SecureFolderFS . Sdk . Services ;
21+ using SecureFolderFS . Sdk . ViewModels ;
1922using SecureFolderFS . Sdk . ViewModels . Views . Host ;
2023using SecureFolderFS . Sdk . ViewModels . Views . Root ;
2124using SecureFolderFS . Shared ;
@@ -154,12 +157,13 @@ await SafetyHelpers.NoFailureAsync(async () =>
154157#if WINDOWS
155158 // Check if the app was launched via file activation (shortcut file)
156159 var isShortcutActivation = IsShortcutFileActivation ( Program . InitialActivationArgs ) ;
160+ var isUriActivation = IsUriActivation ( Program . InitialActivationArgs ) ;
157161
158162 // Activate MainWindow (required for initialization)
159163 MainWindow . Activate ( ) ;
160164
161165 // If launched via shortcut file, hide the main window immediately
162- if ( isShortcutActivation )
166+ if ( isShortcutActivation || isUriActivation )
163167 MainWindow . Hide ( enableEfficiencyMode : false ) ;
164168
165169 // Process initial file activation if the app was launched via file association
@@ -190,24 +194,36 @@ private static bool IsShortcutFileActivation(AppActivationArguments? args)
190194 return file is IStorageFile storageFile &&
191195 storageFile . Path . EndsWith ( UI . Constants . FileNames . VAULT_SHORTCUT_FILE_EXTENSION , StringComparison . OrdinalIgnoreCase ) ;
192196 }
197+
198+ private static bool IsUriActivation ( AppActivationArguments ? args )
199+ {
200+ return args is { Kind : ExtendedActivationKind . Protocol , Data : IProtocolActivatedEventArgs } ;
201+ }
193202#endif
194203
195204 /// <summary>
196205 /// Invoked when the application is activated by opening a file.
197206 /// </summary>
198207 public async Task OnActivatedAsync ( AppActivationArguments args )
199208 {
200- if ( args . Kind != ExtendedActivationKind . File )
201- return ;
209+ if ( args . Kind == ExtendedActivationKind . File )
210+ {
211+ if ( args . Data is not IFileActivatedEventArgs fileArgs )
212+ return ;
202213
203- if ( args . Data is not IFileActivatedEventArgs fileArgs )
204- return ;
214+ var file = fileArgs . Files . Count > 0 ? fileArgs . Files [ 0 ] : null ;
215+ if ( file is not IStorageFile storageFile || ! storageFile . Path . EndsWith ( Constants . FileNames . VAULT_SHORTCUT_FILE_EXTENSION , StringComparison . OrdinalIgnoreCase ) )
216+ return ;
205217
206- var file = fileArgs . Files . Count > 0 ? fileArgs . Files [ 0 ] : null ;
207- if ( file is not IStorageFile storageFile || ! storageFile . Path . EndsWith ( Constants . FileNames . VAULT_SHORTCUT_FILE_EXTENSION , StringComparison . OrdinalIgnoreCase ) )
208- return ;
218+ await HandleVaultShortcutActivationAsync ( storageFile . Path ) ;
219+ }
220+ else if ( args . Kind == ExtendedActivationKind . Protocol )
221+ {
222+ if ( args . Data is not IProtocolActivatedEventArgs protocolArgs )
223+ return ;
209224
210- await HandleVaultShortcutActivationAsync ( storageFile . Path ) ;
225+ await HandleUriActivationAsync ( protocolArgs . Uri ) ;
226+ }
211227 }
212228
213229 /// <summary>
@@ -220,14 +236,21 @@ public async Task HandleVaultShortcutActivationAsync(string filePath)
220236 await using var shortcutStream = await shortcutFile . OpenReadAsync ( default ) ;
221237
222238 var shortcutData = await SerializationExtensions . DeserializeAsync < Stream , VaultShortcutDataModel > ( StreamSerializer . Instance , shortcutStream ) ;
223- if ( shortcutData ? . PersistableId is null || MainViewModel is null )
239+ if ( shortcutData ? . PersistableId is null )
240+ return ;
241+
242+ await HandleVaultPreviewActivationAsync ( shortcutData . PersistableId ) ;
243+ }
244+
245+ private async Task HandleVaultPreviewActivationAsync ( string persistableId )
246+ {
247+ if ( MainViewModel is null )
224248 return ;
225249
226- // Wait for the main window to finish initializing (vault collection loaded, navigation set up)
227250 await MainWindowInitialized . Task ;
228251
229- // Find the vault in the collection by PersistableId
230- var listItemViewModel = MainViewModel . VaultListViewModel . Items . FirstOrDefault ( x => x . VaultViewModel . VaultModel . DataModel . PersistableId == shortcutData . PersistableId ) ;
252+ var listItemViewModel = MainViewModel . VaultListViewModel . Items . FirstOrDefault ( x =>
253+ x . VaultViewModel . VaultModel . DataModel . PersistableId == persistableId ) ;
231254 if ( listItemViewModel is null )
232255 return ;
233256
@@ -237,11 +260,9 @@ await MainWindowSynchronizationContext.PostOrExecuteAsync(async () =>
237260 if ( MainViewModel . RootNavigationService . CurrentView is not MainHostViewModel mainHostViewModel )
238261 return ;
239262
240- // Create the preview window
241263 var window = new Window ( ) ;
242264 window . Closed += PreviewWindow_Closed ;
243265
244- // Initialize preview view model
245266 var title = $ "{ nameof ( SecureFolderFS ) } - { listItemViewModel . VaultViewModel . Title } ";
246267 var vaultPreviewViewModel = ! vaultViewModel . IsUnlocked
247268 ? new VaultPreviewViewModel ( vaultViewModel , mainHostViewModel . NavigationService )
@@ -254,18 +275,13 @@ await MainWindowSynchronizationContext.PostOrExecuteAsync(async () =>
254275 EnsureEarlyWindow ( window , title ) ;
255276
256277#if WINDOWS
257- // Get BoundsManager
258278 var boundsManager = Platforms . Windows . Helpers . WindowsBoundsManager . AddOrGet ( window ) ;
259-
260- // Set minimum window size
261279 boundsManager . MinWidth = 464 ;
262280 boundsManager . MinHeight = 640 ;
263281 window . AppWindow . MoveAndResize ( new ( 100 , 100 , 464 , 640 ) ) ;
264282#endif
265283
266- // Initialize the login view model
267284 await vaultPreviewViewModel . InitAsync ( ) ;
268-
269285 window . Activate ( ) ;
270286 } ) ;
271287
@@ -279,6 +295,54 @@ static void PreviewWindow_Closed(object sender, WindowEventArgs args)
279295 }
280296 }
281297
298+ private async Task HandleVaultLockActivationAsync ( string persistableId )
299+ {
300+ if ( MainViewModel is null )
301+ return ;
302+
303+ await MainWindowInitialized . Task ;
304+
305+ var listItemViewModel = MainViewModel . VaultListViewModel . Items . FirstOrDefault ( x =>
306+ x . VaultViewModel . VaultModel . DataModel . PersistableId == persistableId ) ;
307+ if ( listItemViewModel is null )
308+ return ;
309+
310+ var vaultViewModel = listItemViewModel . VaultViewModel ;
311+ if ( ! vaultViewModel . IsUnlocked )
312+ return ;
313+
314+ await MainWindowSynchronizationContext . PostOrExecuteAsync ( async ( ) =>
315+ {
316+ WeakReferenceMessenger . Default . Send ( new VaultLockRequestedMessage ( vaultViewModel . VaultModel ) ) ;
317+ } ) ;
318+ }
319+
320+ /// <summary>
321+ /// Handles URI protocol activation (e.g. sffs://vault/preview?id=...).
322+ /// </summary>
323+ public async Task HandleUriActivationAsync ( Uri uri )
324+ {
325+ if ( ! uri . Host . Equals ( "vault" , StringComparison . OrdinalIgnoreCase ) )
326+ return ;
327+
328+ var query = System . Web . HttpUtility . ParseQueryString ( uri . Query ) ;
329+ var persistableId = query [ "id" ] ;
330+ if ( persistableId is null )
331+ return ;
332+
333+ var action = uri . AbsolutePath . Trim ( '/' ) ;
334+ switch ( action )
335+ {
336+ case "preview" :
337+ await HandleVaultPreviewActivationAsync ( persistableId ) ;
338+ break ;
339+
340+ case "lock" :
341+ await HandleVaultLockActivationAsync ( persistableId ) ;
342+ break ;
343+ }
344+ }
345+
282346 #region Window Configuration
283347
284348 private static void EnsureEarlyWindow ( Window window , string title )
0 commit comments