2 /* -----------------------------------------------------------------------
3 * <copyright file="ShellViewModel.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
42 using System.Collections.Concurrent;
43 using System.Diagnostics;
44 using System.Diagnostics.Contracts;
47 using System.Reflection;
48 using System.Runtime.InteropServices;
49 using System.ServiceModel;
50 using System.Threading.Tasks;
52 using System.Windows.Controls.Primitives;
53 using AppLimit.NetSparkle;
55 using Hardcodet.Wpf.TaskbarNotification;
56 using Pithos.Client.WPF.Configuration;
57 using Pithos.Client.WPF.FileProperties;
58 using Pithos.Client.WPF.Preferences;
59 using Pithos.Client.WPF.SelectiveSynch;
60 using Pithos.Client.WPF.Services;
61 using Pithos.Client.WPF.Shell;
63 using Pithos.Core.Agents;
64 using Pithos.Interfaces;
66 using System.Collections.Generic;
69 using StatusService = Pithos.Client.WPF.Services.StatusService;
71 namespace Pithos.Client.WPF {
72 using System.ComponentModel.Composition;
76 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
77 /// The shell also hosts the status service called by shell extensions to retrieve file info
80 /// It is a strange "shell" as its main visible element is an icon instead of a window
81 /// The shell subscribes to the following events:
82 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
83 /// * SelectiveSynchChanges: Notifies that the user made changes to the selective synch folders for an account. Raised by the Selective Synch dialog. Located here because the monitors are here
84 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
86 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
87 [Export(typeof(IShell)), Export(typeof(ShellViewModel))]
88 public class ShellViewModel : Screen, IStatusNotification, IShell,
89 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
92 //The Status Checker provides the current synch state
93 //TODO: Could we remove the status checker and use events in its place?
94 private readonly IStatusChecker _statusChecker;
95 private readonly IEventAggregator _events;
97 public PithosSettings Settings { get; private set; }
100 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
102 /// Dictionary of account monitors, keyed by account
105 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
106 /// retrieve account and boject info
108 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
109 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
110 public ConcurrentDictionary<Uri, PithosMonitor> Monitors
112 get { return _monitors; }
117 /// The status service is used by Shell extensions to retrieve file status information
119 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
120 private ServiceHost _statusService;
122 //Logging in the Pithos client is provided by log4net
123 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
125 private readonly PollAgent _pollAgent;
128 private MiniStatusViewModel _miniStatus;
131 public MiniStatusViewModel MiniStatus
133 get { return _miniStatus; }
137 _miniStatus.Shell = this;
138 _miniStatus.Deactivated += (sender, arg) =>
140 _statusVisible = false;
141 NotifyOfPropertyChange(()=>MiniStatusCaption);
147 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
150 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
152 [ImportingConstructor]
153 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
158 _windowManager = windowManager;
159 //CHECK: Caliburn doesn't need explicit command construction
160 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
161 _statusChecker = statusChecker;
164 _events.Subscribe(this);
166 _pollAgent = pollAgent;
169 Proxy.SetFromSettings(settings);
171 StatusMessage = Settings.Accounts.Count==0
172 ? "No Accounts added\r\nPlease add an account"
175 _accounts.CollectionChanged += (sender, e) =>
177 NotifyOfPropertyChange(() => OpenFolderCaption);
178 NotifyOfPropertyChange(() => HasAccounts);
183 catch (Exception exc)
185 Log.Error("Error while starting the ShellViewModel",exc);
191 private void SetVersionMessage()
193 Assembly assembly = Assembly.GetExecutingAssembly();
194 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
195 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
198 public void OnStatusAction()
200 if (Accounts.Count==0)
202 ShowPreferences("AccountTab");
205 protected override void OnActivate()
211 //Must delay opening the upgrade window
212 //to avoid Windows Messages sent by the TaskbarIcon
213 TaskEx.Delay(5000).ContinueWith(_=>
214 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
221 private void OnCheckFinished(object sender, bool updaterequired)
224 Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
225 if (_manualUpgradeCheck)
227 _manualUpgradeCheck = false;
229 //Sparkle raises events on a background thread
230 Execute.OnUIThread(()=>
231 ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
235 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
237 Log.InfoFormat("Update detected {0}",e.LatestVersion);
240 public void CheckForUpgrade()
242 ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
244 _sparkle.updateDetected -= OnUpgradeDetected;
245 _sparkle.checkLoopFinished -= OnCheckFinished;
248 _manualUpgradeCheck = true;
250 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
253 private void InitializeSparkle()
255 _sparkle = new Sparkle(Settings.UpdateUrl);
256 _sparkle.updateDetected += OnUpgradeDetected;
257 _sparkle.checkLoopFinished += OnCheckFinished;
258 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
261 private async void StartMonitoring()
265 if (Settings.IgnoreCertificateErrors)
267 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
270 var accounts = Settings.Accounts.Select(MonitorAccount);
271 await TaskEx.WhenAll(accounts);
272 _statusService = StatusService.Start();
275 catch (AggregateException exc)
279 Log.Error("Error while starting monitoring", e);
286 protected override void OnDeactivate(bool close)
288 base.OnDeactivate(close);
291 StatusService.Stop(_statusService);
292 _statusService = null;
296 public Task MonitorAccount(AccountSettings account)
298 return Task.Factory.StartNew(() =>
300 PithosMonitor monitor;
301 var accountName = account.AccountName;
303 if (Monitors.TryGetValue(account.AccountKey, out monitor))
305 //If the account is active
306 if (account.IsActive)
308 //The Api Key may have changed throuth the Preferences dialog
309 monitor.ApiKey = account.ApiKey;
310 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
311 monitor.RootPath = account.RootPath;
312 //Start the monitor. It's OK to start an already started monitor,
313 //it will just ignore the call
314 StartMonitor(monitor).Wait();
318 //If the account is inactive
319 //Stop and remove the monitor
320 RemoveMonitor(account.ServerUrl,accountName);
326 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
327 monitor = new PithosMonitor
329 UserName = accountName,
330 ApiKey = account.ApiKey,
331 StatusNotification = this,
332 RootPath = account.RootPath
334 //PithosMonitor uses MEF so we need to resolve it
335 IoC.BuildUp(monitor);
337 monitor.AuthenticationUrl = account.ServerUrl;
339 Monitors[account.AccountKey] = monitor;
341 if (account.IsActive)
343 //Don't start a monitor if it doesn't have an account and ApiKey
344 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
345 String.IsNullOrWhiteSpace(monitor.ApiKey))
347 StartMonitor(monitor);
353 protected override void OnViewLoaded(object view)
356 var window = (Window)view;
357 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
358 base.OnViewLoaded(view);
362 #region Status Properties
364 private string _statusMessage;
365 public string StatusMessage
367 get { return _statusMessage; }
370 _statusMessage = value;
371 NotifyOfPropertyChange(() => StatusMessage);
372 NotifyOfPropertyChange(() => TooltipMessage);
376 public string VersionMessage { get; set; }
378 public string TooltipMessage
382 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
386 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
387 public ObservableConcurrentCollection<AccountInfo> Accounts
389 get { return _accounts; }
392 public bool HasAccounts
394 get { return _accounts.Count > 0; }
398 public string OpenFolderCaption
402 return (_accounts.Count == 0)
403 ? "No Accounts Defined"
404 : "Open Pithos Folder";
408 private string _pauseSyncCaption="Pause Synching";
409 public string PauseSyncCaption
411 get { return _pauseSyncCaption; }
414 _pauseSyncCaption = value;
415 NotifyOfPropertyChange(() => PauseSyncCaption);
419 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
420 public ObservableConcurrentCollection<FileEntry> RecentFiles
422 get { return _recentFiles; }
426 private string _statusIcon="../Images/Pithos.ico";
427 public string StatusIcon
429 get { return _statusIcon; }
432 //TODO: Ensure all status icons use the Pithos logo
434 NotifyOfPropertyChange(() => StatusIcon);
442 public void ShowPreferences()
444 ShowPreferences(null);
447 public void ShowPreferences(string currentTab)
450 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
451 _windowManager.ShowDialog(preferences);
455 public void AboutPithos()
457 var about = IoC.Get<AboutViewModel>();
458 about.LatestVersion=_sparkle.LatestVersion;
459 _windowManager.ShowWindow(about);
462 public void SendFeedback()
464 var feedBack = IoC.Get<FeedbackViewModel>();
465 _windowManager.ShowWindow(feedBack);
468 //public PithosCommand OpenPithosFolderCommand { get; private set; }
470 public void OpenPithosFolder()
472 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
475 Process.Start(account.RootPath);
478 public void OpenPithosFolder(AccountInfo account)
480 Process.Start(account.AccountPath);
485 public void GoToSite()
487 var site = Properties.Settings.Default.ProductionServer;
492 public void GoToSite(AccountInfo account)
494 var uri = account.SiteUri.Replace("http://","https://");
498 private bool _statusVisible;
500 public string MiniStatusCaption
504 return _statusVisible ? "Hide Status Window" : "Show Status Window";
508 public void ShowMiniStatus()
511 _windowManager.ShowWindow(MiniStatus);
514 if (MiniStatus.IsActive)
515 MiniStatus.TryClose();
517 _statusVisible=!_statusVisible;
519 NotifyOfPropertyChange(()=>MiniStatusCaption);
522 public bool HasConflicts
526 public void ShowConflicts()
528 _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());
532 /// Open an explorer window to the target path's directory
533 /// and select the file
535 /// <param name="entry"></param>
536 public void GoToFile(FileEntry entry)
538 var fullPath = entry.FullPath;
539 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
541 Process.Start("explorer.exe","/select, " + fullPath);
544 public void OpenLogPath()
546 var pithosDataPath = PithosSettings.PithosDataPath;
548 Process.Start(pithosDataPath);
551 public void ShowFileProperties()
553 var account = Settings.Accounts.First(acc => acc.IsActive);
554 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
555 var files=dir.GetFiles();
557 var idx=r.Next(0, files.Length);
558 ShowFileProperties(files[idx].FullName);
561 public void ShowFileProperties(string filePath)
563 if (String.IsNullOrWhiteSpace(filePath))
564 throw new ArgumentNullException("filePath");
565 if (!File.Exists(filePath) && !Directory.Exists(filePath))
566 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
567 Contract.EndContractBlock();
569 var pair=(from monitor in Monitors
570 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
571 select monitor).FirstOrDefault();
572 var accountMonitor = pair.Value;
574 if (accountMonitor == null)
577 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
581 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
582 _windowManager.ShowWindow(fileProperties);
585 public void ShowContainerProperties()
587 var account = Settings.Accounts.First(acc => acc.IsActive);
588 var dir = new DirectoryInfo(account.RootPath);
589 var fullName = (from folder in dir.EnumerateDirectories()
590 where (folder.Attributes & FileAttributes.Hidden) == 0
591 select folder.FullName).First();
592 ShowContainerProperties(fullName);
595 public void ShowContainerProperties(string filePath)
597 if (String.IsNullOrWhiteSpace(filePath))
598 throw new ArgumentNullException("filePath");
599 if (!Directory.Exists(filePath))
600 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
601 Contract.EndContractBlock();
603 var pair=(from monitor in Monitors
604 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
605 select monitor).FirstOrDefault();
606 var accountMonitor = pair.Value;
607 var info = accountMonitor.GetContainerInfo(filePath);
611 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
612 _windowManager.ShowWindow(containerProperties);
615 public void SynchNow()
617 _pollAgent.SynchNow();
620 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
622 if (currentInfo==null)
623 throw new ArgumentNullException("currentInfo");
624 Contract.EndContractBlock();
625 var monitor = Monitors[currentInfo.AccountKey];
626 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
630 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
632 if (container == null)
633 throw new ArgumentNullException("container");
634 Contract.EndContractBlock();
636 var monitor = Monitors[container.AccountKey];
637 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
642 public void ToggleSynching()
645 foreach (var pair in Monitors)
647 var monitor = pair.Value;
648 monitor.Pause = !monitor.Pause;
649 isPaused = monitor.Pause;
653 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
654 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
655 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
658 public void ExitPithos()
663 foreach (var monitor in Monitors.Select(pair => pair.Value))
668 var view = GetView() as Window;
672 catch (Exception exc)
674 Log.Info("Exception while exiting", exc);
678 Application.Current.Shutdown();
685 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
687 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
688 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
689 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
690 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
691 }.ToDictionary(s => s.Status);
693 readonly IWindowManager _windowManager;
695 //private int _syncCount=0;
698 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
700 public void SetPithosStatus(PithosStatus status)
702 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
704 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
706 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
707 _pithosStatus = PithosStatus.InSynch;
709 _pithosStatus = status;
713 public void SetPithosStatus(PithosStatus status,string message)
715 StatusMessage = message;
716 SetPithosStatus(status);
719 /* public Notifier GetNotifier(Notification startNotification, Notification endNotification)
721 return new Notifier(this, startNotification, endNotification);
724 public Notifier GetNotifier(string startMessage, string endMessage, params object[] args)
726 return new Notifier(this,
727 new StatusNotification(String.Format(startMessage,args)),
728 new StatusNotification(String.Format(endMessage,args)));
733 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
735 public void UpdateStatus()
738 if (_iconNames.ContainsKey(_pithosStatus))
740 var info = _iconNames[_pithosStatus];
741 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
744 if (_pithosStatus == PithosStatus.InSynch)
745 StatusMessage = "All files up to date";
750 private Task StartMonitor(PithosMonitor monitor,int retries=0)
752 return Task.Factory.StartNew(() =>
754 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
758 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
762 catch (WebException exc)
764 if (AbandonRetry(monitor, retries))
767 HttpStatusCode statusCode =HttpStatusCode.OK;
768 var response = exc.Response as HttpWebResponse;
770 statusCode = response.StatusCode;
774 case HttpStatusCode.Unauthorized:
775 var message = String.Format("API Key Expired for {0}. Starting Renewal",
777 Log.Error(message, exc);
778 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
779 account.IsExpired = true;
780 Notify(new ExpirationNotification(account));
781 //TryAuthorize(monitor.UserName, retries).Wait();
783 case HttpStatusCode.ProxyAuthenticationRequired:
784 TryAuthenticateProxy(monitor,retries);
787 TryLater(monitor, exc, retries);
791 catch (Exception exc)
793 if (AbandonRetry(monitor, retries))
796 TryLater(monitor,exc,retries);
802 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
804 Execute.OnUIThread(() =>
806 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
807 proxyAccount.Settings = Settings;
808 if (true != _windowManager.ShowDialog(proxyAccount))
810 StartMonitor(monitor, retries);
811 NotifyOfPropertyChange(() => Accounts);
815 private bool AbandonRetry(PithosMonitor monitor, int retries)
819 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
821 _events.Publish(new Notification
822 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
829 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
831 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
832 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
833 _events.Publish(new Notification
834 {Title = "Error", Message = message, Level = TraceLevel.Error});
835 Log.Error(message, exc);
839 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
841 StatusMessage = status;
843 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
846 public void NotifyChangedFile(string filePath)
848 if (RecentFiles.Any(e => e.FullPath == filePath))
851 IProducerConsumerCollection<FileEntry> files=RecentFiles;
853 while (files.Count > 5)
854 files.TryTake(out popped);
855 var entry = new FileEntry { FullPath = filePath };
859 public void NotifyAccount(AccountInfo account)
863 //TODO: What happens to an existing account whose Token has changed?
864 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
865 account.SiteUri, Uri.EscapeDataString(account.Token),
866 Uri.EscapeDataString(account.UserName));
868 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
869 Accounts.TryAdd(account);
873 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
875 if (conflictFiles == null)
877 //Convert to list to avoid multiple iterations
878 var files = conflictFiles.ToList();
883 //TODO: Create a more specific message. For now, just show a warning
884 NotifyForFiles(files,message,TraceLevel.Warning);
888 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
895 StatusMessage = message;
897 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
900 public void Notify(Notification notification)
902 _events.Publish(notification);
906 public void RemoveMonitor(string serverUrl,string accountName)
908 if (String.IsNullOrWhiteSpace(accountName))
911 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
912 if (accountInfo != null)
914 _accounts.TryRemove(accountInfo);
915 _pollAgent.RemoveAccount(accountInfo);
918 var accountKey = new Uri(new Uri(serverUrl),accountName);
919 PithosMonitor monitor;
920 if (Monitors.TryRemove(accountKey, out monitor))
923 //TODO: Also remove any pending actions for this account
924 //from the network queue
928 public void RefreshOverlays()
930 foreach (var pair in Monitors)
932 var monitor = pair.Value;
934 var path = monitor.RootPath;
936 if (String.IsNullOrWhiteSpace(path))
939 if (!Directory.Exists(path) && !File.Exists(path))
942 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
946 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
947 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
948 pathPointer, IntPtr.Zero);
952 Marshal.FreeHGlobal(pathPointer);
957 #region Event Handlers
959 public void Handle(SelectiveSynchChanges message)
961 PithosMonitor monitor;
962 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
964 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
971 private bool _pollStarted;
972 private Sparkle _sparkle;
973 private bool _manualUpgradeCheck;
975 //SMELL: Doing so much work for notifications in the shell is wrong
976 //The notifications should be moved to their own view/viewmodel pair
977 //and different templates should be used for different message types
978 //This will also allow the addition of extra functionality, eg. actions
980 public void Handle(Notification notification)
984 if (!Settings.ShowDesktopNotifications)
987 if (notification is PollNotification)
992 if (notification is CloudNotification)
997 notification.Title = "Pithos+";
998 notification.Message = "Start Synchronisation";
1001 var deleteNotification = notification as CloudDeleteNotification;
1002 if (deleteNotification != null)
1004 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
1008 var progress = notification as ProgressNotification;
1011 if (progress != null)
1013 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
1015 progress.Block/(double)progress.TotalBlocks,
1016 progress.FileSize.ToByteSize(),
1021 var info = notification as StatusNotification;
1024 StatusMessage = info.Title;
1027 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1030 if (notification.Level <= TraceLevel.Warning)
1031 ShowBalloonFor(notification);
1034 private void ShowBalloonFor(Notification notification)
1036 Contract.Requires(notification!=null);
1038 if (!Settings.ShowDesktopNotifications)
1042 switch (notification.Level)
1044 case TraceLevel.Verbose:
1046 case TraceLevel.Info:
1047 icon = BalloonIcon.Info;
1049 case TraceLevel.Error:
1050 icon = BalloonIcon.Error;
1052 case TraceLevel.Warning:
1053 icon = BalloonIcon.Warning;
1059 var tv = (ShellView) GetView();
1060 System.Action clickAction = null;
1061 if (notification is ExpirationNotification)
1063 clickAction = () => ShowPreferences("AccountTab");
1065 var balloon = new PithosBalloon
1067 Title = notification.Title,
1068 Message = notification.Message,
1070 ClickAction = clickAction
1072 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1077 public void Handle(ShowFilePropertiesEvent message)
1079 if (message == null)
1080 throw new ArgumentNullException("message");
1081 if (String.IsNullOrWhiteSpace(message.FileName) )
1082 throw new ArgumentException("message");
1083 Contract.EndContractBlock();
1085 var fileName = message.FileName;
1086 //TODO: Display file properties for non-container folders
1087 if (File.Exists(fileName))
1088 //Retrieve the full name with exact casing. Pithos names are case sensitive
1089 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1090 else if (Directory.Exists(fileName))
1091 //Retrieve the full name with exact casing. Pithos names are case sensitive
1093 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1094 if (IsContainer(path))
1095 ShowContainerProperties(path);
1097 ShowFileProperties(path);
1101 private bool IsContainer(string path)
1103 var matchingFolders = from account in _accounts
1104 from rootFolder in Directory.GetDirectories(account.AccountPath)
1105 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1107 return matchingFolders.Any();
1110 public FileStatus GetFileStatus(string localFileName)
1112 if (String.IsNullOrWhiteSpace(localFileName))
1113 throw new ArgumentNullException("localFileName");
1114 Contract.EndContractBlock();
1116 var statusKeeper = IoC.Get<IStatusKeeper>();
1117 var status=statusKeeper.GetFileStatus(localFileName);
1121 public void RemoveAccountFromDatabase(AccountSettings account)
1123 var statusKeeper = IoC.Get<IStatusKeeper>();
1124 statusKeeper.ClearFolderStatus(account.RootPath);