#region /* ----------------------------------------------------------------------- * * * Copyright 2011-2012 GRNET S.A. All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above * copyright notice, this list of conditions and the following * disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and * documentation are those of the authors and should not be * interpreted as representing official policies, either expressed * or implied, of GRNET S.A. * * ----------------------------------------------------------------------- */ #endregion using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Contracts; using System.IO; using System.Net; using System.Reflection; using System.Runtime.InteropServices; using System.ServiceModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls.Primitives; using AppLimit.NetSparkle; using Caliburn.Micro; using Hardcodet.Wpf.TaskbarNotification; using Pithos.Client.WPF.Configuration; using Pithos.Client.WPF.FileProperties; using Pithos.Client.WPF.Preferences; using Pithos.Client.WPF.SelectiveSynch; using Pithos.Client.WPF.Services; using Pithos.Client.WPF.Shell; using Pithos.Core; using Pithos.Core.Agents; using Pithos.Interfaces; using System; using System.Collections.Generic; using System.Linq; using Pithos.Network; using StatusService = Pithos.Client.WPF.Services.StatusService; namespace Pithos.Client.WPF { using System.ComponentModel.Composition; /// /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications. /// The shell also hosts the status service called by shell extensions to retrieve file info /// /// /// It is a strange "shell" as its main visible element is an icon instead of a window /// The shell subscribes to the following events: /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon /// * 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 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog /// //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges? [Export(typeof(IShell)), Export(typeof(ShellViewModel))] public class ShellViewModel : Screen, IStatusNotification, IShell, IHandle, IHandle, IHandle { //The Status Checker provides the current synch state //TODO: Could we remove the status checker and use events in its place? private readonly IStatusChecker _statusChecker; private readonly IEventAggregator _events; public PithosSettings Settings { get; private set; } private readonly ConcurrentDictionary _monitors = new ConcurrentDictionary(); /// /// Dictionary of account monitors, keyed by account /// /// /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands, /// retrieve account and boject info /// // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design? // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier public ConcurrentDictionary Monitors { get { return _monitors; } } /// /// The status service is used by Shell extensions to retrieve file status information /// //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell private ServiceHost _statusService; //Logging in the Pithos client is provided by log4net private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private readonly PollAgent _pollAgent; private MiniStatusViewModel _miniStatus; [Import] public MiniStatusViewModel MiniStatus { get { return _miniStatus; } set { _miniStatus = value; _miniStatus.Shell = this; _miniStatus.Deactivated += (sender, arg) => { _statusVisible = false; NotifyOfPropertyChange(()=>MiniStatusCaption); }; } } /// /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings /// /// /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry) /// [ImportingConstructor] public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings,PollAgent pollAgent) { try { _windowManager = windowManager; //CHECK: Caliburn doesn't need explicit command construction //OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); _statusChecker = statusChecker; //The event subst _events = events; _events.Subscribe(this); _pollAgent = pollAgent; Settings = settings; Proxy.SetFromSettings(settings); StatusMessage = Settings.Accounts.Count==0 ? "No Accounts added\r\nPlease add an account" : "Starting"; _accounts.CollectionChanged += (sender, e) => { NotifyOfPropertyChange(() => OpenFolderCaption); NotifyOfPropertyChange(() => HasAccounts); }; SetVersionMessage(); } catch (Exception exc) { Log.Error("Error while starting the ShellViewModel",exc); throw; } } private void SetVersionMessage() { Assembly assembly = Assembly.GetExecutingAssembly(); var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location); VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion); } public void OnStatusAction() { if (Accounts.Count==0) { ShowPreferences("AccountTab"); } } protected override void OnActivate() { base.OnActivate(); InitializeSparkle(); //Must delay opening the upgrade window //to avoid Windows Messages sent by the TaskbarIcon TaskEx.Delay(5000).ContinueWith(_=> Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval))); StartMonitoring(); } private void OnCheckFinished(object sender, bool updaterequired) { Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired); if (_manualUpgradeCheck) { _manualUpgradeCheck = false; if (!updaterequired) //Sparkle raises events on a background thread Execute.OnUIThread(()=> ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"})); } } private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e) { Log.InfoFormat("Update detected {0}",e.LatestVersion); } public void CheckForUpgrade() { ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."}); _sparkle.StopLoop(); _sparkle.updateDetected -= OnUpgradeDetected; _sparkle.checkLoopFinished -= OnCheckFinished; _sparkle.Dispose(); _manualUpgradeCheck = true; InitializeSparkle(); _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval); } private void InitializeSparkle() { _sparkle = new Sparkle(Settings.UpdateUrl); _sparkle.updateDetected += OnUpgradeDetected; _sparkle.checkLoopFinished += OnCheckFinished; _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics; } private async void StartMonitoring() { try { if (Settings.IgnoreCertificateErrors) { ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true; } var accounts = Settings.Accounts.Select(MonitorAccount); await TaskEx.WhenAll(accounts); _statusService = StatusService.Start(); } catch (AggregateException exc) { exc.Handle(e => { Log.Error("Error while starting monitoring", e); return true; }); throw; } } protected override void OnDeactivate(bool close) { base.OnDeactivate(close); if (close) { StatusService.Stop(_statusService); _statusService = null; } } public Task MonitorAccount(AccountSettings account) { return Task.Factory.StartNew(() => { PithosMonitor monitor; var accountName = account.AccountName; if (Monitors.TryGetValue(account.AccountKey, out monitor)) { //If the account is active if (account.IsActive) { //The Api Key may have changed throuth the Preferences dialog monitor.ApiKey = account.ApiKey; Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object"); monitor.RootPath = account.RootPath; //Start the monitor. It's OK to start an already started monitor, //it will just ignore the call StartMonitor(monitor).Wait(); } else { //If the account is inactive //Stop and remove the monitor RemoveMonitor(account.ServerUrl,accountName); } return; } //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors monitor = new PithosMonitor { UserName = accountName, ApiKey = account.ApiKey, StatusNotification = this, RootPath = account.RootPath }; //PithosMonitor uses MEF so we need to resolve it IoC.BuildUp(monitor); monitor.AuthenticationUrl = account.ServerUrl; Monitors[account.AccountKey] = monitor; if (account.IsActive) { //Don't start a monitor if it doesn't have an account and ApiKey if (String.IsNullOrWhiteSpace(monitor.UserName) || String.IsNullOrWhiteSpace(monitor.ApiKey)) return; StartMonitor(monitor); } }); } protected override void OnViewLoaded(object view) { UpdateStatus(); var window = (Window)view; TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide)); base.OnViewLoaded(view); } #region Status Properties private string _statusMessage; public string StatusMessage { get { return _statusMessage; } set { _statusMessage = value; NotifyOfPropertyChange(() => StatusMessage); NotifyOfPropertyChange(() => TooltipMessage); } } public string VersionMessage { get; set; } public string TooltipMessage { get { return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage); } } private readonly ObservableConcurrentCollection _accounts = new ObservableConcurrentCollection(); public ObservableConcurrentCollection Accounts { get { return _accounts; } } public bool HasAccounts { get { return _accounts.Count > 0; } } public string OpenFolderCaption { get { return (_accounts.Count == 0) ? "No Accounts Defined" : "Open Pithos Folder"; } } private string _pauseSyncCaption="Pause Synching"; public string PauseSyncCaption { get { return _pauseSyncCaption; } set { _pauseSyncCaption = value; NotifyOfPropertyChange(() => PauseSyncCaption); } } private readonly ObservableConcurrentCollection _recentFiles = new ObservableConcurrentCollection(); public ObservableConcurrentCollection RecentFiles { get { return _recentFiles; } } private string _statusIcon="../Images/Pithos.ico"; public string StatusIcon { get { return _statusIcon; } set { //TODO: Ensure all status icons use the Pithos logo _statusIcon = value; NotifyOfPropertyChange(() => StatusIcon); } } #endregion #region Commands public void ShowPreferences() { ShowPreferences(null); } public void ShowPreferences(string currentTab) { //Settings.Reload(); var preferences = new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab); _windowManager.ShowDialog(preferences); } public void AboutPithos() { var about = IoC.Get(); about.LatestVersion=_sparkle.LatestVersion; _windowManager.ShowWindow(about); } public void SendFeedback() { var feedBack = IoC.Get(); _windowManager.ShowWindow(feedBack); } //public PithosCommand OpenPithosFolderCommand { get; private set; } public void OpenPithosFolder() { var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive); if (account == null) return; Process.Start(account.RootPath); } public void OpenPithosFolder(AccountInfo account) { Process.Start(account.AccountPath); } public void GoToSite() { var site = Properties.Settings.Default.ProductionServer; Process.Start(site); } public void GoToSite(AccountInfo account) { var uri = account.SiteUri.Replace("http://","https://"); Process.Start(uri); } private bool _statusVisible; public string MiniStatusCaption { get { return _statusVisible ? "Hide Status Window" : "Show Status Window"; } } public void ShowMiniStatus() { if (!_statusVisible) _windowManager.ShowWindow(MiniStatus); else { if (MiniStatus.IsActive) MiniStatus.TryClose(); } _statusVisible=!_statusVisible; NotifyOfPropertyChange(()=>MiniStatusCaption); } public bool HasConflicts { get { return true; } } public void ShowConflicts() { _windowManager.ShowWindow(IoC.Get()); } /// /// Open an explorer window to the target path's directory /// and select the file /// /// public void GoToFile(FileEntry entry) { var fullPath = entry.FullPath; if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) return; Process.Start("explorer.exe","/select, " + fullPath); } public void OpenLogPath() { var pithosDataPath = PithosSettings.PithosDataPath; Process.Start(pithosDataPath); } public void ShowFileProperties() { var account = Settings.Accounts.First(acc => acc.IsActive); var dir = new DirectoryInfo(account.RootPath + @"\pithos"); var files=dir.GetFiles(); var r=new Random(); var idx=r.Next(0, files.Length); ShowFileProperties(files[idx].FullName); } public void ShowFileProperties(string filePath) { if (String.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!File.Exists(filePath) && !Directory.Exists(filePath)) throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); Contract.EndContractBlock(); var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); var accountMonitor = pair.Value; if (accountMonitor == null) return; var infoTask=Task.Factory.StartNew(()=>accountMonitor.GetObjectInfo(filePath)); var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath); _windowManager.ShowWindow(fileProperties); } public void ShowContainerProperties() { var account = Settings.Accounts.First(acc => acc.IsActive); var dir = new DirectoryInfo(account.RootPath); var fullName = (from folder in dir.EnumerateDirectories() where (folder.Attributes & FileAttributes.Hidden) == 0 select folder.FullName).First(); ShowContainerProperties(fullName); } public void ShowContainerProperties(string filePath) { if (String.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!Directory.Exists(filePath)) throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath"); Contract.EndContractBlock(); var pair=(from monitor in Monitors where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase) select monitor).FirstOrDefault(); var accountMonitor = pair.Value; var info = accountMonitor.GetContainerInfo(filePath); var containerProperties = new ContainerPropertiesViewModel(this, info,filePath); _windowManager.ShowWindow(containerProperties); } public void SynchNow() { _pollAgent.SynchNow(); } public ObjectInfo RefreshObjectInfo(ObjectInfo currentInfo) { if (currentInfo==null) throw new ArgumentNullException("currentInfo"); Contract.EndContractBlock(); var monitor = Monitors[currentInfo.AccountKey]; var newInfo=monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name); return newInfo; } public ContainerInfo RefreshContainerInfo(ContainerInfo container) { if (container == null) throw new ArgumentNullException("container"); Contract.EndContractBlock(); var monitor = Monitors[container.AccountKey]; var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name); return newInfo; } public void ToggleSynching() { bool isPaused=false; foreach (var pair in Monitors) { var monitor = pair.Value; monitor.Pause = !monitor.Pause; isPaused = monitor.Pause; } PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; StatusIcon = String.Format(@"../Images/{0}.ico", iconKey); } public void ExitPithos() { try { foreach (var monitor in Monitors.Select(pair => pair.Value)) { monitor.Stop(); } var view = GetView() as Window; if (view != null) view.Close(); } catch (Exception exc) { Log.Info("Exception while exiting", exc); } finally { Application.Current.Shutdown(); } } #endregion private readonly Dictionary _iconNames = new List { new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"), new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"), new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") }.ToDictionary(s => s.Status); readonly IWindowManager _windowManager; //private int _syncCount=0; private PithosStatus _pithosStatus = PithosStatus.Disconnected; public void SetPithosStatus(PithosStatus status) { if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete) return; if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete) return; if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete) _pithosStatus = PithosStatus.InSynch; else _pithosStatus = status; UpdateStatus(); } public void SetPithosStatus(PithosStatus status,string message) { StatusMessage = message; SetPithosStatus(status); } /* public Notifier GetNotifier(Notification startNotification, Notification endNotification) { return new Notifier(this, startNotification, endNotification); }*/ public Notifier GetNotifier(string startMessage, string endMessage, params object[] args) { return new Notifier(this, new StatusNotification(String.Format(startMessage,args)), new StatusNotification(String.Format(endMessage,args))); } /// /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat /// public void UpdateStatus() { if (_iconNames.ContainsKey(_pithosStatus)) { var info = _iconNames[_pithosStatus]; StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName); } if (_pithosStatus == PithosStatus.InSynch) StatusMessage = "All files up to date"; } private Task StartMonitor(PithosMonitor monitor,int retries=0) { return Task.Factory.StartNew(() => { using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) { try { Log.InfoFormat("Start Monitoring {0}", monitor.UserName); monitor.Start(); } catch (WebException exc) { if (AbandonRetry(monitor, retries)) return; HttpStatusCode statusCode =HttpStatusCode.OK; var response = exc.Response as HttpWebResponse; if(response!=null) statusCode = response.StatusCode; switch (statusCode) { case HttpStatusCode.Unauthorized: var message = String.Format("API Key Expired for {0}. Starting Renewal", monitor.UserName); Log.Error(message, exc); var account = Settings.Accounts.Find(acc => acc.AccountName == monitor.UserName); account.IsExpired = true; Notify(new ExpirationNotification(account)); //TryAuthorize(monitor.UserName, retries).Wait(); break; case HttpStatusCode.ProxyAuthenticationRequired: TryAuthenticateProxy(monitor,retries); break; default: TryLater(monitor, exc, retries); break; } } catch (Exception exc) { if (AbandonRetry(monitor, retries)) return; TryLater(monitor,exc,retries); } } }); } private void TryAuthenticateProxy(PithosMonitor monitor,int retries) { Execute.OnUIThread(() => { var proxyAccount = IoC.Get(); proxyAccount.Settings = Settings; if (true != _windowManager.ShowDialog(proxyAccount)) return; StartMonitor(monitor, retries); NotifyOfPropertyChange(() => Accounts); }); } private bool AbandonRetry(PithosMonitor monitor, int retries) { if (retries > 1) { var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry", monitor.UserName); _events.Publish(new Notification {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error}); return true; } return false; } private void TryLater(PithosMonitor monitor, Exception exc,int retries) { var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1)); _events.Publish(new Notification {Title = "Error", Message = message, Level = TraceLevel.Error}); Log.Error(message, exc); } public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) { StatusMessage = status; _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level }); } public void NotifyChangedFile(string filePath) { if (RecentFiles.Any(e => e.FullPath == filePath)) return; IProducerConsumerCollection files=RecentFiles; FileEntry popped; while (files.Count > 5) files.TryTake(out popped); var entry = new FileEntry { FullPath = filePath }; files.TryAdd(entry); } public void NotifyAccount(AccountInfo account) { if (account== null) return; //TODO: What happens to an existing account whose Token has changed? account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}", account.SiteUri, Uri.EscapeDataString(account.Token), Uri.EscapeDataString(account.UserName)); if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri)) Accounts.TryAdd(account); } public void NotifyConflicts(IEnumerable conflictFiles, string message) { if (conflictFiles == null) return; //Convert to list to avoid multiple iterations var files = conflictFiles.ToList(); if (files.Count==0) return; UpdateStatus(); //TODO: Create a more specific message. For now, just show a warning NotifyForFiles(files,message,TraceLevel.Warning); } public void NotifyForFiles(IEnumerable files, string message,TraceLevel level=TraceLevel.Info) { if (files == null) return; if (!files.Any()) return; StatusMessage = message; _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level}); } public void Notify(Notification notification) { _events.Publish(notification); } public void RemoveMonitor(string serverUrl,string accountName) { if (String.IsNullOrWhiteSpace(accountName)) return; var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl)); if (accountInfo != null) { _accounts.TryRemove(accountInfo); _pollAgent.RemoveAccount(accountInfo); } var accountKey = new Uri(new Uri(serverUrl),accountName); PithosMonitor monitor; if (Monitors.TryRemove(accountKey, out monitor)) { monitor.Stop(); //TODO: Also remove any pending actions for this account //from the network queue } } public void RefreshOverlays() { foreach (var pair in Monitors) { var monitor = pair.Value; var path = monitor.RootPath; if (String.IsNullOrWhiteSpace(path)) continue; if (!Directory.Exists(path) && !File.Exists(path)) continue; IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); try { NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, pathPointer, IntPtr.Zero); } finally { Marshal.FreeHGlobal(pathPointer); } } } #region Event Handlers public void Handle(SelectiveSynchChanges message) { PithosMonitor monitor; if (Monitors.TryGetValue(message.Account.AccountKey, out monitor)) { monitor.SetSelectivePaths(message.Uris,message.Added,message.Removed); } var account = Accounts.First(acc => acc.AccountKey == message.Account.AccountKey); this._pollAgent.SetSelectivePaths(account, message.Added, message.Removed); } private bool _pollStarted; private Sparkle _sparkle; private bool _manualUpgradeCheck; //SMELL: Doing so much work for notifications in the shell is wrong //The notifications should be moved to their own view/viewmodel pair //and different templates should be used for different message types //This will also allow the addition of extra functionality, eg. actions // public void Handle(Notification notification) { UpdateStatus(); if (!Settings.ShowDesktopNotifications) return; if (notification is PollNotification) { _pollStarted = true; return; } if (notification is CloudNotification) { if (!_pollStarted) return; _pollStarted= false; notification.Title = "Pithos+"; notification.Message = "Start Synchronisation"; } var deleteNotification = notification as CloudDeleteNotification; if (deleteNotification != null) { StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name); return; } var progress = notification as ProgressNotification; if (progress != null) { StatusMessage = String.Format("{0} {1:p2} of {2} - {3}", progress.Action, progress.Block/(double)progress.TotalBlocks, progress.FileSize.ToByteSize(), progress.FileName); return; } var info = notification as StatusNotification; if (info != null) { StatusMessage = info.Title; return; } if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title)) return; if (notification.Level <= TraceLevel.Warning) ShowBalloonFor(notification); } private void ShowBalloonFor(Notification notification) { Contract.Requires(notification!=null); if (!Settings.ShowDesktopNotifications) return; BalloonIcon icon; switch (notification.Level) { case TraceLevel.Verbose: return; case TraceLevel.Info: icon = BalloonIcon.Info; break; case TraceLevel.Error: icon = BalloonIcon.Error; break; case TraceLevel.Warning: icon = BalloonIcon.Warning; break; default: return; } var tv = (ShellView) GetView(); System.Action clickAction = null; if (notification is ExpirationNotification) { clickAction = () => ShowPreferences("AccountTab"); } var balloon = new PithosBalloon { Title = notification.Title, Message = notification.Message, Icon = icon, ClickAction = clickAction }; tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000); } #endregion public void Handle(ShowFilePropertiesEvent message) { if (message == null) throw new ArgumentNullException("message"); if (String.IsNullOrWhiteSpace(message.FileName) ) throw new ArgumentException("message"); Contract.EndContractBlock(); var fileName = message.FileName; //TODO: Display file properties for non-container folders if (File.Exists(fileName)) //Retrieve the full name with exact casing. Pithos names are case sensitive ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName)); else if (Directory.Exists(fileName)) //Retrieve the full name with exact casing. Pithos names are case sensitive { var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName); if (IsContainer(path)) ShowContainerProperties(path); else ShowFileProperties(path); } } private bool IsContainer(string path) { var matchingFolders = from account in _accounts from rootFolder in Directory.GetDirectories(account.AccountPath) where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase) select rootFolder; return matchingFolders.Any(); } public FileStatus GetFileStatus(string localFileName) { if (String.IsNullOrWhiteSpace(localFileName)) throw new ArgumentNullException("localFileName"); Contract.EndContractBlock(); var statusKeeper = IoC.Get(); var status=statusKeeper.GetFileStatus(localFileName); return status; } public void RemoveAccountFromDatabase(AccountSettings account) { var statusKeeper = IoC.Get(); statusKeeper.ClearFolderStatus(account.RootPath); } } }