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))]
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<string, PithosMonitor> _monitors = new ConcurrentDictionary<string, 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<string, 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 //Lazily initialized File Version info. This is done once and lazily to avoid blocking the UI
126 private readonly Lazy<FileVersionInfo> _fileVersion;
128 private readonly PollAgent _pollAgent;
131 private MiniStatusViewModel _miniStatus;
134 public MiniStatusViewModel MiniStatus
136 get { return _miniStatus; }
140 _miniStatus.Shell = this;
145 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
148 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
150 [ImportingConstructor]
151 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent)
156 _windowManager = windowManager;
157 //CHECK: Caliburn doesn't need explicit command construction
158 //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder);
159 _statusChecker = statusChecker;
162 _events.Subscribe(this);
164 _pollAgent = pollAgent;
167 Proxy.SetFromSettings(settings);
169 StatusMessage = Settings.Accounts.Count==0
170 ? "No Accounts added. Please add an account"
173 _fileVersion= new Lazy<FileVersionInfo>(() =>
175 Assembly assembly = Assembly.GetExecutingAssembly();
176 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
179 _accounts.CollectionChanged += (sender, e) =>
181 NotifyOfPropertyChange(() => OpenFolderCaption);
182 NotifyOfPropertyChange(() => HasAccounts);
186 catch (Exception exc)
188 Log.Error("Error while starting the ShellViewModel",exc);
195 protected override void OnActivate()
199 _sparkle = new Sparkle(Settings.UpdateUrl);
200 _sparkle.updateDetected += OnUpgradeDetected;
201 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
203 //Must delay opening the upgrade window
204 //to avoid Windows Messages sent by the TaskbarIcon
205 TaskEx.Delay(5000).ContinueWith(_=>
206 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
212 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
214 Log.InfoFormat("Update detected {0}",e.LatestVersion);
217 public void CheckForUpgrade()
219 Log.Error("Test Error message");
222 _sparkle=new Sparkle(Settings.UpdateUrl);
223 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
226 private async void StartMonitoring()
230 var accounts = Settings.Accounts.Select(MonitorAccount);
231 await TaskEx.WhenAll(accounts);
232 _statusService = StatusService.Start();
235 foreach (var account in Settings.Accounts)
237 await MonitorAccount(account);
242 catch (AggregateException exc)
246 Log.Error("Error while starting monitoring", e);
253 protected override void OnDeactivate(bool close)
255 base.OnDeactivate(close);
258 StatusService.Stop(_statusService);
259 _statusService = null;
263 public Task MonitorAccount(AccountSettings account)
265 return Task.Factory.StartNew(() =>
267 PithosMonitor monitor;
268 var accountName = account.AccountName;
270 if (_monitors.TryGetValue(accountName, out monitor))
272 //If the account is active
273 if (account.IsActive)
275 //The Api Key may have changed throuth the Preferences dialog
276 monitor.ApiKey = account.ApiKey;
277 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
278 monitor.RootPath = account.RootPath;
279 //Start the monitor. It's OK to start an already started monitor,
280 //it will just ignore the call
281 StartMonitor(monitor).Wait();
285 //If the account is inactive
286 //Stop and remove the monitor
287 RemoveMonitor(accountName);
293 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
294 monitor = new PithosMonitor
296 UserName = accountName,
297 ApiKey = account.ApiKey,
298 StatusNotification = this,
299 RootPath = account.RootPath
301 //PithosMonitor uses MEF so we need to resolve it
302 IoC.BuildUp(monitor);
304 monitor.AuthenticationUrl = account.ServerUrl;
306 _monitors[accountName] = monitor;
308 if (account.IsActive)
310 //Don't start a monitor if it doesn't have an account and ApiKey
311 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
312 String.IsNullOrWhiteSpace(monitor.ApiKey))
314 StartMonitor(monitor);
320 protected override void OnViewLoaded(object view)
323 var window = (Window)view;
324 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
325 base.OnViewLoaded(view);
329 #region Status Properties
331 private string _statusMessage;
332 public string StatusMessage
334 get { return _statusMessage; }
337 _statusMessage = value;
338 NotifyOfPropertyChange(() => StatusMessage);
342 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
343 public ObservableConcurrentCollection<AccountInfo> Accounts
345 get { return _accounts; }
348 public bool HasAccounts
350 get { return _accounts.Count > 0; }
354 public string OpenFolderCaption
358 return (_accounts.Count == 0)
359 ? "No Accounts Defined"
360 : "Open Pithos Folder";
364 private string _pauseSyncCaption="Pause Synching";
365 public string PauseSyncCaption
367 get { return _pauseSyncCaption; }
370 _pauseSyncCaption = value;
371 NotifyOfPropertyChange(() => PauseSyncCaption);
375 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
376 public ObservableConcurrentCollection<FileEntry> RecentFiles
378 get { return _recentFiles; }
382 private string _statusIcon="../Images/Pithos.ico";
383 public string StatusIcon
385 get { return _statusIcon; }
388 //TODO: Ensure all status icons use the Pithos logo
390 NotifyOfPropertyChange(() => StatusIcon);
398 public void ShowPreferences()
400 ShowPreferences(null);
403 public void ShowPreferences(string currentTab)
406 var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
407 _windowManager.ShowDialog(preferences);
411 public void AboutPithos()
413 var about = new AboutViewModel();
414 _windowManager.ShowWindow(about);
417 public void SendFeedback()
419 var feedBack = IoC.Get<FeedbackViewModel>();
420 _windowManager.ShowWindow(feedBack);
423 //public PithosCommand OpenPithosFolderCommand { get; private set; }
425 public void OpenPithosFolder()
427 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
430 Process.Start(account.RootPath);
433 public void OpenPithosFolder(AccountInfo account)
435 Process.Start(account.AccountPath);
440 public void GoToSite()
442 var site = Properties.Settings.Default.PithosSite;
447 public void GoToSite(AccountInfo account)
449 var uri = account.SiteUri.Replace("http://","https://");
453 private bool _statusVisible;
455 public string MiniStatusCaption
459 return _statusVisible ? "Hide Status Window" : "Show Status Window";
463 public void ShowMiniStatus()
466 _windowManager.ShowWindow(MiniStatus);
469 MiniStatus.TryClose();
471 _statusVisible=!_statusVisible;
473 NotifyOfPropertyChange(()=>MiniStatusCaption);
477 /// Open an explorer window to the target path's directory
478 /// and select the file
480 /// <param name="entry"></param>
481 public void GoToFile(FileEntry entry)
483 var fullPath = entry.FullPath;
484 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
486 Process.Start("explorer.exe","/select, " + fullPath);
489 public void OpenLogPath()
491 var pithosDataPath = PithosSettings.PithosDataPath;
493 Process.Start(pithosDataPath);
496 public void ShowFileProperties()
498 var account = Settings.Accounts.First(acc => acc.IsActive);
499 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
500 var files=dir.GetFiles();
502 var idx=r.Next(0, files.Length);
503 ShowFileProperties(files[idx].FullName);
506 public void ShowFileProperties(string filePath)
508 if (String.IsNullOrWhiteSpace(filePath))
509 throw new ArgumentNullException("filePath");
510 if (!File.Exists(filePath) && !Directory.Exists(filePath))
511 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
512 Contract.EndContractBlock();
514 var pair=(from monitor in Monitors
515 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
516 select monitor).FirstOrDefault();
517 var accountMonitor = pair.Value;
519 if (accountMonitor == null)
522 var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath));
526 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
527 _windowManager.ShowWindow(fileProperties);
530 public void ShowContainerProperties()
532 var account = Settings.Accounts.First(acc => acc.IsActive);
533 var dir = new DirectoryInfo(account.RootPath);
534 var fullName = (from folder in dir.EnumerateDirectories()
535 where (folder.Attributes & FileAttributes.Hidden) == 0
536 select folder.FullName).First();
537 ShowContainerProperties(fullName);
540 public void ShowContainerProperties(string filePath)
542 if (String.IsNullOrWhiteSpace(filePath))
543 throw new ArgumentNullException("filePath");
544 if (!Directory.Exists(filePath))
545 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
546 Contract.EndContractBlock();
548 var pair=(from monitor in Monitors
549 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
550 select monitor).FirstOrDefault();
551 var accountMonitor = pair.Value;
552 var info = accountMonitor.GetContainerInfo(filePath);
556 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
557 _windowManager.ShowWindow(containerProperties);
560 public void SynchNow()
562 _pollAgent.SynchNow();
565 public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo)
567 if (currentInfo==null)
568 throw new ArgumentNullException("currentInfo");
569 Contract.EndContractBlock();
571 var monitor = Monitors[currentInfo.Account];
572 var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name);
576 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
578 if (container == null)
579 throw new ArgumentNullException("container");
580 Contract.EndContractBlock();
582 var monitor = Monitors[container.Account];
583 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
588 public void ToggleSynching()
591 foreach (var pair in Monitors)
593 var monitor = pair.Value;
594 monitor.Pause = !monitor.Pause;
595 isPaused = monitor.Pause;
599 PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing";
600 var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch";
601 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
604 public void ExitPithos()
606 foreach (var pair in Monitors)
608 var monitor = pair.Value;
612 ((Window)GetView()).Close();
617 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
619 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
620 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
621 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
622 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
623 }.ToDictionary(s => s.Status);
625 readonly IWindowManager _windowManager;
627 //private int _syncCount=0;
630 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
632 public void SetPithosStatus(PithosStatus status)
634 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
636 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
638 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
639 _pithosStatus = PithosStatus.InSynch;
641 _pithosStatus = status;
645 public void SetPithosStatus(PithosStatus status,string message)
647 StatusMessage = message;
648 SetPithosStatus(status);
654 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
656 public void UpdateStatus()
659 if (_iconNames.ContainsKey(_pithosStatus))
661 var info = _iconNames[_pithosStatus];
662 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
665 if (_pithosStatus == PithosStatus.InSynch)
666 StatusMessage = "All files up to date";
671 private Task StartMonitor(PithosMonitor monitor,int retries=0)
673 return Task.Factory.StartNew(() =>
675 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
679 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
683 catch (WebException exc)
685 if (AbandonRetry(monitor, retries))
688 HttpStatusCode statusCode =HttpStatusCode.OK;
689 var response = exc.Response as HttpWebResponse;
691 statusCode = response.StatusCode;
695 case HttpStatusCode.Unauthorized:
696 var message = String.Format("API Key Expired for {0}. Starting Renewal",
698 Log.Error(message, exc);
699 var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName);
700 account.IsExpired = true;
701 Notify(new ExpirationNotification(account));
702 //TryAuthorize(monitor.UserName, retries).Wait();
704 case HttpStatusCode.ProxyAuthenticationRequired:
705 TryAuthenticateProxy(monitor,retries);
708 TryLater(monitor, exc, retries);
712 catch (Exception exc)
714 if (AbandonRetry(monitor, retries))
717 TryLater(monitor,exc,retries);
723 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
725 Execute.OnUIThread(() =>
727 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
728 proxyAccount.Settings = Settings;
729 if (true != _windowManager.ShowDialog(proxyAccount))
731 StartMonitor(monitor, retries);
732 NotifyOfPropertyChange(() => Accounts);
736 private bool AbandonRetry(PithosMonitor monitor, int retries)
740 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
742 _events.Publish(new Notification
743 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
750 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
752 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
753 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
754 _events.Publish(new Notification
755 {Title = "Error", Message = message, Level = TraceLevel.Error});
756 Log.Error(message, exc);
760 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
762 StatusMessage = status;
764 _events.Publish(new Notification { Title = "Pithos", Message = status, Level = level });
767 public void NotifyChangedFile(string filePath)
769 if (RecentFiles.Any(e => e.FullPath == filePath))
772 IProducerConsumerCollection<FileEntry> files=RecentFiles;
774 while (files.Count > 5)
775 files.TryTake(out popped);
776 var entry = new FileEntry { FullPath = filePath };
780 public void NotifyAccount(AccountInfo account)
784 //TODO: What happens to an existing account whose Token has changed?
785 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
786 account.SiteUri, Uri.EscapeDataString(account.Token),
787 Uri.EscapeDataString(account.UserName));
789 if (Accounts.All(item => item.UserName != account.UserName))
790 Accounts.TryAdd(account);
794 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
796 if (conflictFiles == null)
798 //Convert to list to avoid multiple iterations
799 var files = conflictFiles.ToList();
804 //TODO: Create a more specific message. For now, just show a warning
805 NotifyForFiles(files,message,TraceLevel.Warning);
809 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
816 StatusMessage = message;
818 _events.Publish(new Notification { Title = "Pithos", Message = message, Level = level});
821 public void Notify(Notification notification)
823 _events.Publish(notification);
827 public void RemoveMonitor(string accountName)
829 if (String.IsNullOrWhiteSpace(accountName))
832 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName);
833 if (accountInfo != null)
835 _accounts.TryRemove(accountInfo);
836 _pollAgent.RemoveAccount(accountInfo);
839 PithosMonitor monitor;
840 if (Monitors.TryRemove(accountName, out monitor))
843 //TODO: Also remove any pending actions for this account
844 //from the network queue
848 public void RefreshOverlays()
850 foreach (var pair in Monitors)
852 var monitor = pair.Value;
854 var path = monitor.RootPath;
856 if (String.IsNullOrWhiteSpace(path))
859 if (!Directory.Exists(path) && !File.Exists(path))
862 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
866 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
867 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
868 pathPointer, IntPtr.Zero);
872 Marshal.FreeHGlobal(pathPointer);
877 #region Event Handlers
879 public void Handle(SelectiveSynchChanges message)
881 var accountName = message.Account.AccountName;
882 PithosMonitor monitor;
883 if (_monitors.TryGetValue(accountName, out monitor))
885 monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed);
892 private bool _pollStarted;
893 private Sparkle _sparkle;
895 //SMELL: Doing so much work for notifications in the shell is wrong
896 //The notifications should be moved to their own view/viewmodel pair
897 //and different templates should be used for different message types
898 //This will also allow the addition of extra functionality, eg. actions
900 public void Handle(Notification notification)
904 if (!Settings.ShowDesktopNotifications)
907 if (notification is PollNotification)
912 if (notification is CloudNotification)
917 notification.Title = "Pithos";
918 notification.Message = "Start Synchronisation";
921 var deleteNotification = notification as CloudDeleteNotification;
922 if (deleteNotification != null)
924 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
928 var progress = notification as ProgressNotification;
929 if (progress != null)
931 StatusMessage = String.Format("Pithos {0}\r\n{1} {2:p2} of {3} - {4}",
932 _fileVersion.Value.FileVersion,
934 progress.Block/(double)progress.TotalBlocks,
935 progress.FileSize.ToByteSize(),
940 var info = notification as StatusNotification;
943 StatusMessage = String.Format("Pithos {0}\r\n{1}",
944 _fileVersion.Value.FileVersion,
948 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
951 ShowBalloonFor(notification);
954 private void ShowBalloonFor(Notification notification)
956 Contract.Requires(notification!=null);
958 if (!Settings.ShowDesktopNotifications)
962 switch (notification.Level)
964 case TraceLevel.Info:
965 case TraceLevel.Verbose:
967 case TraceLevel.Error:
968 icon = BalloonIcon.Error;
970 case TraceLevel.Warning:
971 icon = BalloonIcon.Warning;
977 var tv = (ShellView) GetView();
978 System.Action clickAction = null;
979 if (notification is ExpirationNotification)
981 clickAction = () => ShowPreferences("AccountTab");
983 var balloon = new PithosBalloon
985 Title = notification.Title,
986 Message = notification.Message,
988 ClickAction = clickAction
990 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
995 public void Handle(ShowFilePropertiesEvent message)
998 throw new ArgumentNullException("message");
999 if (String.IsNullOrWhiteSpace(message.FileName) )
1000 throw new ArgumentException("message");
1001 Contract.EndContractBlock();
1003 var fileName = message.FileName;
1004 //TODO: Display file properties for non-container folders
1005 if (File.Exists(fileName))
1006 //Retrieve the full name with exact casing. Pithos names are case sensitive
1007 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
1008 else if (Directory.Exists(fileName))
1009 //Retrieve the full name with exact casing. Pithos names are case sensitive
1011 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
1012 if (IsContainer(path))
1013 ShowContainerProperties(path);
1015 ShowFileProperties(path);
1019 private bool IsContainer(string path)
1021 var matchingFolders = from account in _accounts
1022 from rootFolder in Directory.GetDirectories(account.AccountPath)
1023 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
1025 return matchingFolders.Any();
1028 public FileStatus GetFileStatus(string localFileName)
1030 if (String.IsNullOrWhiteSpace(localFileName))
1031 throw new ArgumentNullException("localFileName");
1032 Contract.EndContractBlock();
1034 var statusKeeper = IoC.Get<IStatusKeeper>();
1035 var status=statusKeeper.GetFileStatus(localFileName);
1039 public void RemoveAccountFromDatabase(AccountSettings account)
1041 var statusKeeper = IoC.Get<IStatusKeeper>();
1042 statusKeeper.ClearFolderStatus(account.RootPath);