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 Application.Current.Shutdown();
674 catch (Exception exc)
676 Log.Info("Exception while exiting", exc);
677 Application.Current.Shutdown();
684 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
686 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
687 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
688 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
689 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
690 }.ToDictionary(s => s.Status);
692 readonly IWindowManager _windowManager;
694 //private int _syncCount=0;
697 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
699 public void SetPithosStatus(PithosStatus status)
701 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
703 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
705 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
706 _pithosStatus = PithosStatus.InSynch;
708 _pithosStatus = status;
712 public void SetPithosStatus(PithosStatus status,string message)
714 StatusMessage = message;
715 SetPithosStatus(status);
718 /* public Notifier GetNotifier(Notification startNotification, Notification endNotification)
720 return new Notifier(this, startNotification, endNotification);
723 public Notifier GetNotifier(string startMessage, string endMessage, params object[] args)
725 return new Notifier(this,
726 new StatusNotification(String.Format(startMessage,args)),
727 new StatusNotification(String.Format(endMessage,args)));
732 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
734 public void UpdateStatus()
737 if (_iconNames.ContainsKey(_pithosStatus))
739 var info = _iconNames[_pithosStatus];
740 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
743 if (_pithosStatus == PithosStatus.InSynch)
744 StatusMessage = "All files up to date";
749 private Task StartMonitor(PithosMonitor monitor,int retries=0)
751 return Task.Factory.StartNew(() =>
753 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
757 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
761 catch (WebException exc)
763 if (AbandonRetry(monitor, retries))
766 HttpStatusCode statusCode =HttpStatusCode.OK;
767 var response = exc.Response as HttpWebResponse;
769 statusCode = response.StatusCode;
773 case HttpStatusCode.Unauthorized:
774 var message = String.Format("API Key Expired for {0}. Starting Renewal",
776 Log.Error(message, exc);
777 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
778 account.IsExpired = true;
779 Notify(new ExpirationNotification(account));
780 //TryAuthorize(monitor.UserName, retries).Wait();
782 case HttpStatusCode.ProxyAuthenticationRequired:
783 TryAuthenticateProxy(monitor,retries);
786 TryLater(monitor, exc, retries);
790 catch (Exception exc)
792 if (AbandonRetry(monitor, retries))
795 TryLater(monitor,exc,retries);
801 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
803 Execute.OnUIThread(() =>
805 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
806 proxyAccount.Settings = Settings;
807 if (true != _windowManager.ShowDialog(proxyAccount))
809 StartMonitor(monitor, retries);
810 NotifyOfPropertyChange(() => Accounts);
814 private bool AbandonRetry(PithosMonitor monitor, int retries)
818 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
820 _events.Publish(new Notification
821 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
828 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
830 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
831 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
832 _events.Publish(new Notification
833 {Title = "Error", Message = message, Level = TraceLevel.Error});
834 Log.Error(message, exc);
838 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
840 StatusMessage = status;
842 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
845 public void NotifyChangedFile(string filePath)
847 if (RecentFiles.Any(e => e.FullPath == filePath))
850 IProducerConsumerCollection<FileEntry> files=RecentFiles;
852 while (files.Count > 5)
853 files.TryTake(out popped);
854 var entry = new FileEntry { FullPath = filePath };
858 public void NotifyAccount(AccountInfo account)
862 //TODO: What happens to an existing account whose Token has changed?
863 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
864 account.SiteUri, Uri.EscapeDataString(account.Token),
865 Uri.EscapeDataString(account.UserName));
867 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
868 Accounts.TryAdd(account);
872 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
874 if (conflictFiles == null)
876 //Convert to list to avoid multiple iterations
877 var files = conflictFiles.ToList();
882 //TODO: Create a more specific message. For now, just show a warning
883 NotifyForFiles(files,message,TraceLevel.Warning);
887 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
894 StatusMessage = message;
896 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
899 public void Notify(Notification notification)
901 _events.Publish(notification);
905 public void RemoveMonitor(string serverUrl,string accountName)
907 if (String.IsNullOrWhiteSpace(accountName))
910 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
911 if (accountInfo != null)
913 _accounts.TryRemove(accountInfo);
914 _pollAgent.RemoveAccount(accountInfo);
917 var accountKey = new Uri(new Uri(serverUrl),accountName);
918 PithosMonitor monitor;
919 if (Monitors.TryRemove(accountKey, out monitor))
922 //TODO: Also remove any pending actions for this account
923 //from the network queue
927 public void RefreshOverlays()
929 foreach (var pair in Monitors)
931 var monitor = pair.Value;
933 var path = monitor.RootPath;
935 if (String.IsNullOrWhiteSpace(path))
938 if (!Directory.Exists(path) && !File.Exists(path))
941 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
945 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
946 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
947 pathPointer, IntPtr.Zero);
951 Marshal.FreeHGlobal(pathPointer);
956 #region Event Handlers
958 public void Handle(SelectiveSynchChanges message)
960 PithosMonitor monitor;
961 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
963 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
970 private bool _pollStarted;
971 private Sparkle _sparkle;
972 private bool _manualUpgradeCheck;
974 //SMELL: Doing so much work for notifications in the shell is wrong
975 //The notifications should be moved to their own view/viewmodel pair
976 //and different templates should be used for different message types
977 //This will also allow the addition of extra functionality, eg. actions
979 public void Handle(Notification notification)
983 if (!Settings.ShowDesktopNotifications)
986 if (notification is PollNotification)
991 if (notification is CloudNotification)
996 notification.Title = "Pithos+";
997 notification.Message = "Start Synchronisation";
1000 var deleteNotification = notification as CloudDeleteNotification;
1001 if (deleteNotification != null)
1003 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
1007 var progress = notification as ProgressNotification;
1010 if (progress != null)
1012 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
1014 progress.Block/(double)progress.TotalBlocks,
1015 progress.FileSize.ToByteSize(),
1020 var info = notification as StatusNotification;
1023 StatusMessage = info.Title;
1026 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
1029 if (notification.Level <= TraceLevel.Warning)
1030 ShowBalloonFor(notification);
1033 private void ShowBalloonFor(Notification notification)
1035 Contract.Requires(notification!=null);
1037 if (!Settings.ShowDesktopNotifications)
1041 switch (notification.Level)
1043 case TraceLevel.Verbose:
1045 case TraceLevel.Info:
1046 icon = BalloonIcon.Info;
1048 case TraceLevel.Error:
1049 icon = BalloonIcon.Error;
1051 case TraceLevel.Warning:
1052 icon = BalloonIcon.Warning;
1058 var tv = (ShellView) GetView();
1059 System.Action clickAction = null;
1060 if (notification is ExpirationNotification)
1062 clickAction = () => ShowPreferences("AccountTab");
1064 var balloon = new PithosBalloon
1066 Title = notification.Title,
1067 Message = notification.Message,
1069 ClickAction = clickAction
1071 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
1076 public void Handle(ShowFilePropertiesEvent message)
1078 if (message == null)
1079 throw new ArgumentNullException("message");
1080 if (String.IsNullOrWhiteSpace(message.FileName) )
1081 throw new ArgumentException("message");
1082 Contract.EndContractBlock();
1084 var fileName = message.FileName;
1085 //TODO: Display file properties for non-container folders
1086 if (File.Exists(fileName))
1087 //Retrieve the full name with exact casing. Pithos names are case sensitive
1088 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1089 else if (Directory.Exists(fileName))
1090 //Retrieve the full name with exact casing. Pithos names are case sensitive
1092 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1093 if (IsContainer(path))
1094 ShowContainerProperties(path);
1096 ShowFileProperties(path);
1100 private bool IsContainer(string path)
1102 var matchingFolders = from account in _accounts
1103 from rootFolder in Directory.GetDirectories(account.AccountPath)
1104 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1106 return matchingFolders.Any();
1109 public FileStatus GetFileStatus(string localFileName)
1111 if (String.IsNullOrWhiteSpace(localFileName))
1112 throw new ArgumentNullException("localFileName");
1113 Contract.EndContractBlock();
1115 var statusKeeper = IoC.Get<IStatusKeeper>();
1116 var status=statusKeeper.GetFileStatus(localFileName);
1120 public void RemoveAccountFromDatabase(AccountSettings account)
1122 var statusKeeper = IoC.Get<IStatusKeeper>();
1123 statusKeeper.ClearFolderStatus(account.RootPath);