2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="ShellViewModel.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
42 using System.Collections.Concurrent;
\r
43 using System.Diagnostics;
\r
44 using System.Diagnostics.Contracts;
\r
47 using System.Reflection;
\r
48 using System.Runtime.InteropServices;
\r
49 using System.ServiceModel;
\r
50 using System.Threading;
\r
51 using System.Threading.Tasks;
\r
52 using System.Windows;
\r
53 using System.Windows.Controls.Primitives;
\r
54 using System.Windows.Input;
\r
55 using AppLimit.NetSparkle;
\r
56 using Caliburn.Micro;
\r
57 using Hardcodet.Wpf.TaskbarNotification;
\r
58 using Pithos.Client.WPF.Configuration;
\r
59 using Pithos.Client.WPF.FileProperties;
\r
60 using Pithos.Client.WPF.Preferences;
\r
61 using Pithos.Client.WPF.SelectiveSynch;
\r
62 using Pithos.Client.WPF.Services;
\r
63 using Pithos.Client.WPF.Shell;
\r
65 using Pithos.Core.Agents;
\r
66 using Pithos.Interfaces;
\r
68 using System.Collections.Generic;
\r
70 using Pithos.Network;
\r
71 using StatusService = Pithos.Client.WPF.Services.StatusService;
\r
73 namespace Pithos.Client.WPF {
\r
74 using System.ComponentModel.Composition;
\r
76 public class ToggleStatusCommand:ICommand
\r
78 private readonly ShellViewModel _model;
\r
79 public ToggleStatusCommand(ShellViewModel model)
\r
83 public void Execute(object parameter)
\r
85 _model.CurrentSyncStatus();
\r
88 public bool CanExecute(object parameter)
\r
93 public event EventHandler CanExecuteChanged;
\r
98 /// The "shell" of the Pithos application displays the taskbar icon, menu and notifications.
\r
99 /// The shell also hosts the status service called by shell extensions to retrieve file info
\r
102 /// It is a strange "shell" as its main visible element is an icon instead of a window
\r
103 /// The shell subscribes to the following events:
\r
104 /// * Notification: Raised by components that want to notify the user. Usually displayed in a balloon
\r
105 /// * 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
\r
106 /// * ShowFilePropertiesEvent: Raised when a shell command requests the display of the file/container properties dialog
\r
108 //TODO: CODE SMELL Why does the shell handle the SelectiveSynchChanges?
\r
109 [Export(typeof(IShell)), Export(typeof(ShellViewModel)),Export(typeof(IStatusNotification))]
\r
110 public class ShellViewModel : Screen, IStatusNotification, IShell,
\r
111 IHandle<Notification>, IHandle<SelectiveSynchChanges>, IHandle<ShowFilePropertiesEvent>
\r
114 private readonly IEventAggregator _events;
\r
116 public PithosSettings Settings { get; private set; }
\r
119 private readonly ConcurrentDictionary<Uri, PithosMonitor> _monitors = new ConcurrentDictionary<Uri, PithosMonitor>();
\r
121 /// Dictionary of account monitors, keyed by account
\r
124 /// One monitor class is created for each account. The Shell needs access to the monitors to execute start/stop/pause commands,
\r
125 /// retrieve account and boject info
\r
127 // TODO: Does the Shell REALLY need access to the monitors? Could we achieve the same results with a better design?
\r
128 // TODO: The monitors should be internal to Pithos.Core, even though exposing them makes coding of the Object and Container windows easier
\r
129 public ConcurrentDictionary<Uri, PithosMonitor> Monitors
\r
131 get { return _monitors; }
\r
136 /// The status service is used by Shell extensions to retrieve file status information
\r
138 //TODO: CODE SMELL! This is the shell! While hosting in the shell makes executing start/stop commands easier, it is still a smell
\r
139 private ServiceHost _statusService;
\r
141 //Logging in the Pithos client is provided by log4net
\r
142 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
145 private PollAgent _pollAgent;
\r
148 private NetworkAgent _networkAgent;
\r
151 public Selectives Selectives { get; set; }
\r
154 public ToggleStatusCommand ToggleMiniStatusCommand { get; set; }
\r
156 private MiniStatusViewModel _miniStatus;
\r
159 public MiniStatusViewModel MiniStatus
\r
161 get { return _miniStatus; }
\r
164 _miniStatus = value;
\r
165 _miniStatus.Shell = this;
\r
166 _miniStatus.Deactivated += (sender, arg) =>
\r
168 _statusVisible = false;
\r
169 NotifyOfPropertyChange(()=>MiniStatusCaption);
\r
175 /// The Shell depends on MEF to provide implementations for windowManager, events, the status checker service and the settings
\r
178 /// The PithosSettings class encapsulates the app's settings to abstract their storage mechanism (App settings, a database or registry)
\r
180 [ImportingConstructor]
\r
181 public ShellViewModel(IWindowManager windowManager, IEventAggregator events, PithosSettings settings/*,PollAgent pollAgent,NetworkAgent networkAgent*/)
\r
186 _windowManager = windowManager;
\r
187 //CHECK: Caliburn doesn't need explicit command construction
\r
188 //CurrentSyncStatusCommand = new PithosCommand(OpenPithosFolder);
\r
191 _events.Subscribe(this);
\r
194 _pollAgent = pollAgent;
\r
195 _networkAgent = networkAgent;
\r
197 Settings = settings;
\r
199 Proxy.SetFromSettings(settings);
\r
201 StatusMessage = Settings.Accounts.Count==0
\r
202 ? "No Accounts added\r\nPlease add an account"
\r
205 _accounts.CollectionChanged += (sender, e) =>
\r
207 NotifyOfPropertyChange(() => OpenFolderCaption);
\r
208 NotifyOfPropertyChange(() => HasAccounts);
\r
211 SetVersionMessage();
\r
213 ToggleMiniStatusCommand=new ToggleStatusCommand(this);
\r
215 catch (Exception exc)
\r
217 Log.Error("Error while starting the ShellViewModel",exc);
\r
223 private void SetVersionMessage()
\r
225 Assembly assembly = Assembly.GetExecutingAssembly();
\r
226 var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
\r
227 VersionMessage = String.Format("Pithos+ {0}", fileVersion.FileVersion);
\r
230 public void CurrentSyncStatus()
\r
232 if (Accounts.Count == 0)
\r
234 ShowPreferences("AccountTab");
\r
238 if (!_statusVisible)
\r
240 _windowManager.ShowWindow(MiniStatus);
\r
241 _statusVisible = true;
\r
245 if (MiniStatus.IsActive)
\r
246 MiniStatus.TryClose();
\r
247 _statusVisible = false;
\r
250 NotifyOfPropertyChange(() => MiniStatusCaption);
\r
254 protected override void OnActivate()
\r
258 InitializeSparkle();
\r
260 //Must delay opening the upgrade window
\r
261 //to avoid Windows Messages sent by the TaskbarIcon
\r
262 TaskEx.Delay(5000).ContinueWith(_=>
\r
263 Execute.OnUIThread(()=> _sparkle.StartLoop(true,Settings.UpdateForceCheck,Settings.UpdateCheckInterval)));
\r
266 StartMonitoring();
\r
270 private void OnCheckFinished(object sender, bool updaterequired)
\r
273 Log.InfoFormat("Upgrade check finished. Need Upgrade: {0}", updaterequired);
\r
274 if (_manualUpgradeCheck)
\r
276 _manualUpgradeCheck = false;
\r
277 if (!updaterequired)
\r
278 //Sparkle raises events on a background thread
\r
279 Execute.OnUIThread(()=>
\r
280 ShowBalloonFor(new Notification{Title="Pithos+ is up to date",Message="You have the latest Pithos+ version. No update is required"}));
\r
284 private void OnUpgradeDetected(object sender, UpdateDetectedEventArgs e)
\r
286 Log.InfoFormat("Update detected {0}",e.LatestVersion);
\r
289 public void CheckForUpgrade()
\r
291 ShowBalloonFor(new Notification{Title="Checking for upgrades",Message="Contacting the server to retrieve the latest Pithos+ version."});
\r
292 _sparkle.StopLoop();
\r
293 _sparkle.updateDetected -= OnUpgradeDetected;
\r
294 _sparkle.checkLoopFinished -= OnCheckFinished;
\r
295 _sparkle.Dispose();
\r
297 _manualUpgradeCheck = true;
\r
298 InitializeSparkle();
\r
299 _sparkle.StartLoop(true,true,Settings.UpdateCheckInterval);
\r
302 private void InitializeSparkle()
\r
304 _sparkle = new Sparkle(Settings.UpdateUrl);
\r
305 _sparkle.updateDetected += OnUpgradeDetected;
\r
306 _sparkle.checkLoopFinished += OnCheckFinished;
\r
307 _sparkle.ShowDiagnosticWindow = Settings.UpdateDiagnostics;
\r
310 private async void StartMonitoring()
\r
314 if (Settings.IgnoreCertificateErrors)
\r
316 ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
\r
319 var accounts = Settings.Accounts.Select(MonitorAccount);
\r
320 await TaskEx.WhenAll(accounts).ConfigureAwait(false);
\r
321 _statusService = StatusService.Start();
\r
324 catch (AggregateException exc)
\r
328 Log.Error("Error while starting monitoring", e);
\r
335 protected override void OnDeactivate(bool close)
\r
337 base.OnDeactivate(close);
\r
340 StatusService.Stop(_statusService);
\r
341 _statusService = null;
\r
345 public Task MonitorAccount(AccountSettings account)
\r
347 return Task.Factory.StartNew(() =>
\r
349 PithosMonitor monitor;
\r
350 var accountName = account.AccountName;
\r
352 MigrateFolders(account);
\r
354 Selectives.SetIsSelectiveEnabled(account.AccountKey, account.SelectiveSyncEnabled);
\r
356 if (Monitors.TryGetValue(account.AccountKey, out monitor))
\r
358 //If the account is active
\r
359 if (account.IsActive)
\r
361 //The Api Key may have changed throuth the Preferences dialog
\r
362 monitor.ApiKey = account.ApiKey;
\r
363 Debug.Assert(monitor.StatusNotification == this,"An existing monitor should already have a StatusNotification service object");
\r
364 monitor.RootPath = account.RootPath;
\r
365 //Start the monitor. It's OK to start an already started monitor,
\r
366 //it will just ignore the call
\r
367 StartMonitor(monitor).Wait();
\r
371 //If the account is inactive
\r
372 //Stop and remove the monitor
\r
373 RemoveMonitor(account.ServerUrl,accountName);
\r
379 //Create a new monitor/ Can't use MEF here, it would return a single instance for all monitors
\r
380 monitor = new PithosMonitor
\r
382 UserName = accountName,
\r
383 ApiKey = account.ApiKey,
\r
384 StatusNotification = this,
\r
385 RootPath = account.RootPath
\r
387 //PithosMonitor uses MEF so we need to resolve it
\r
388 IoC.BuildUp(monitor);
\r
390 monitor.AuthenticationUrl = account.ServerUrl;
\r
392 Monitors[account.AccountKey] = monitor;
\r
394 if (account.IsActive)
\r
396 //Don't start a monitor if it doesn't have an account and ApiKey
\r
397 if (String.IsNullOrWhiteSpace(monitor.UserName) ||
\r
398 String.IsNullOrWhiteSpace(monitor.ApiKey))
\r
400 StartMonitor(monitor);
\r
405 private void MigrateFolders(AccountSettings account)
\r
407 var oldOthersFolder=Path.Combine(account.RootPath, FolderConstants.OldOthersFolder);
\r
408 var newOthersFolder = Path.Combine(account.RootPath, FolderConstants.OthersFolder);
\r
409 var oldFolder = new DirectoryInfo(oldOthersFolder);
\r
410 var newFolder = new DirectoryInfo(newOthersFolder);
\r
412 if (oldFolder.Exists && !newFolder.Exists)
\r
414 oldFolder.MoveTo(newOthersFolder);
\r
419 protected override void OnViewLoaded(object view)
\r
422 var window = (Window)view;
\r
423 TaskEx.Delay(1000).ContinueWith(t => Execute.OnUIThread(window.Hide));
\r
424 base.OnViewLoaded(view);
\r
428 #region Status Properties
\r
430 private string _statusMessage;
\r
431 public string StatusMessage
\r
433 get { return _statusMessage; }
\r
436 _statusMessage = value;
\r
437 NotifyOfPropertyChange(() => StatusMessage);
\r
438 NotifyOfPropertyChange(() => TooltipMessage);
\r
442 public string VersionMessage { get; set; }
\r
444 public string TooltipMessage
\r
448 return String.Format("{0}\r\n{1}",VersionMessage,StatusMessage);
\r
452 public string TooltipMiniStatus
\r
456 return String.Format("{0}\r\n{1}", "Status Window", "Enable / Disable the status window");
\r
460 /*public string ToggleStatusWindowMessage
\r
464 return String.Format("{0}" + Environment.NewLine + "{1} Toggle Mini Status");
\r
468 private readonly ObservableConcurrentCollection<AccountInfo> _accounts = new ObservableConcurrentCollection<AccountInfo>();
\r
469 public ObservableConcurrentCollection<AccountInfo> Accounts
\r
471 get { return _accounts; }
\r
474 public bool HasAccounts
\r
476 get { return _accounts.Count > 0; }
\r
480 public string OpenFolderCaption
\r
484 return (_accounts.Count == 0)
\r
485 ? "No Accounts Defined"
\r
486 : "Open Pithos Folder";
\r
490 private string _pauseSyncCaption="Pause Synching";
\r
491 public string PauseSyncCaption
\r
493 get { return _pauseSyncCaption; }
\r
496 _pauseSyncCaption = value;
\r
497 NotifyOfPropertyChange(() => PauseSyncCaption);
\r
501 private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>();
\r
502 public ObservableConcurrentCollection<FileEntry> RecentFiles
\r
504 get { return _recentFiles; }
\r
508 private string _statusIcon="../Images/Pithos.ico";
\r
509 public string StatusIcon
\r
511 get { return _statusIcon; }
\r
514 //TODO: Ensure all status icons use the Pithos logo
\r
515 _statusIcon = value;
\r
516 NotifyOfPropertyChange(() => StatusIcon);
\r
524 public void CancelCurrentOperation()
\r
526 _pollAgent.CancelCurrentOperation();
\r
529 public void ShowPreferences()
\r
531 ShowPreferences(null);
\r
534 public void ShowPreferences(string currentTab)
\r
536 //Settings.Reload();
\r
538 var preferences = IoC.Get<PreferencesViewModel>();//??new PreferencesViewModel(_windowManager, _events, this, Settings,currentTab);
\r
539 if (!String.IsNullOrWhiteSpace(currentTab))
\r
540 preferences.SelectedTab = currentTab;
\r
541 if (!preferences.IsActive)
\r
542 _windowManager.ShowWindow(preferences);
\r
543 var view = (Window)preferences.GetView();
\r
544 view.NullSafe(v=>v.Activate());
\r
547 public void AboutPithos()
\r
549 var about = IoC.Get<AboutViewModel>();
\r
550 about.LatestVersion=_sparkle.LatestVersion;
\r
551 _windowManager.ShowWindow(about);
\r
554 public void SendFeedback()
\r
556 var feedBack = IoC.Get<FeedbackViewModel>();
\r
557 _windowManager.ShowWindow(feedBack);
\r
560 //public PithosCommand OpenPithosFolderCommand { get; private set; }
\r
562 public void OpenPithosFolder()
\r
564 var account = Settings.Accounts.FirstOrDefault(acc => acc.IsActive);
\r
565 if (account == null)
\r
567 Process.Start(account.RootPath);
\r
570 public void OpenPithosFolder(AccountInfo account)
\r
572 Process.Start(account.AccountPath);
\r
577 public void GoToSite()
\r
579 var site = Properties.Settings.Default.ProductionServer;
\r
580 Process.Start(site);
\r
584 public void GoToSite(AccountInfo account)
\r
586 var uri = account.SiteUri.Replace("http://","https://");
\r
587 Process.Start(uri);
\r
590 private bool _statusVisible;
\r
592 public string MiniStatusCaption
\r
596 return _statusVisible ? "Hide Status Window" : "Show Status Window";
\r
600 public bool HasConflicts
\r
602 get { return true; }
\r
604 public void ShowConflicts()
\r
606 _windowManager.ShowWindow(IoC.Get<ConflictsViewModel>());
\r
610 /// Open an explorer window to the target path's directory
\r
611 /// and select the file
\r
613 /// <param name="entry"></param>
\r
614 public void GoToFile(FileEntry entry)
\r
616 var fullPath = entry.FullPath;
\r
617 if (!File.Exists(fullPath) && !Directory.Exists(fullPath))
\r
619 Process.Start("explorer.exe","/select, " + fullPath);
\r
622 public void OpenLogPath()
\r
624 var pithosDataPath = PithosSettings.PithosDataPath;
\r
626 Process.Start(pithosDataPath);
\r
629 public void ShowFileProperties()
\r
631 var account = Settings.Accounts.First(acc => acc.IsActive);
\r
632 var dir = new DirectoryInfo(account.RootPath + @"\pithos");
\r
633 var files=dir.GetFiles();
\r
634 var r=new Random();
\r
635 var idx=r.Next(0, files.Length);
\r
636 ShowFileProperties(files[idx].FullName);
\r
639 public async void ShowFileProperties(string filePath)
\r
641 if (String.IsNullOrWhiteSpace(filePath))
\r
642 throw new ArgumentNullException("filePath");
\r
643 if (!File.Exists(filePath) && !Directory.Exists(filePath))
\r
644 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
\r
645 Contract.EndContractBlock();
\r
647 var pair=(from monitor in Monitors
\r
648 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
\r
649 select monitor).FirstOrDefault();
\r
650 var accountMonitor = pair.Value;
\r
652 if (accountMonitor == null)
\r
655 var infoTask=accountMonitor.GetObjectInfo(filePath);
\r
659 var fileProperties = new FilePropertiesViewModel(this, infoTask,filePath);
\r
660 _windowManager.ShowWindow(fileProperties);
\r
663 public void ShowContainerProperties()
\r
665 var account = Settings.Accounts.First(acc => acc.IsActive);
\r
666 var dir = new DirectoryInfo(account.RootPath);
\r
667 var fullName = (from folder in dir.EnumerateDirectories()
\r
668 where (folder.Attributes & FileAttributes.Hidden) == 0
\r
669 select folder.FullName).First();
\r
670 ShowContainerProperties(fullName);
\r
673 public void ShowContainerProperties(string filePath)
\r
675 if (String.IsNullOrWhiteSpace(filePath))
\r
676 throw new ArgumentNullException("filePath");
\r
677 if (!Directory.Exists(filePath))
\r
678 throw new ArgumentException(String.Format("Non existent file {0}",filePath),"filePath");
\r
679 Contract.EndContractBlock();
\r
681 var pair=(from monitor in Monitors
\r
682 where filePath.StartsWith(monitor.Value.RootPath, StringComparison.InvariantCultureIgnoreCase)
\r
683 select monitor).FirstOrDefault();
\r
684 var accountMonitor = pair.Value;
\r
685 var info = accountMonitor.GetContainerInfo(filePath);
\r
689 var containerProperties = new ContainerPropertiesViewModel(this, info,filePath);
\r
690 _windowManager.ShowWindow(containerProperties);
\r
693 public void SynchNow()
\r
695 _pollAgent.SynchNow();
\r
698 public async Task<ObjectInfo> RefreshObjectInfo(ObjectInfo currentInfo)
\r
700 if (currentInfo==null)
\r
701 throw new ArgumentNullException("currentInfo");
\r
702 Contract.EndContractBlock();
\r
703 var monitor = Monitors[currentInfo.AccountKey];
\r
704 var newInfo=await monitor.CloudClient.GetObjectInfo(currentInfo.Account, currentInfo.Container, currentInfo.Name).ConfigureAwait(false);
\r
708 public ContainerInfo RefreshContainerInfo(ContainerInfo container)
\r
710 if (container == null)
\r
711 throw new ArgumentNullException("container");
\r
712 Contract.EndContractBlock();
\r
714 var monitor = Monitors[container.AccountKey];
\r
715 var newInfo = monitor.CloudClient.GetContainerInfo(container.Account, container.Name);
\r
719 private bool _isPaused;
\r
720 public bool IsPaused
\r
722 get { return _isPaused; }
\r
726 PauseSyncCaption = IsPaused ? "Resume syncing" : "Pause syncing";
\r
727 var iconKey = IsPaused ? "TraySyncPaused" : "TrayInSynch";
\r
728 StatusIcon = String.Format(@"../Images/{0}.ico", iconKey);
\r
730 NotifyOfPropertyChange(() => IsPaused);
\r
734 public void ToggleSynching()
\r
736 IsPaused=!IsPaused;
\r
737 foreach (var monitor in Monitors.Values)
\r
739 monitor.Pause = IsPaused ;
\r
741 _pollAgent.Pause = IsPaused;
\r
742 _networkAgent.Pause = IsPaused;
\r
747 public void ExitPithos()
\r
752 foreach (var monitor in Monitors.Select(pair => pair.Value))
\r
757 var view = GetView() as Window;
\r
761 catch (Exception exc)
\r
763 Log.Info("Exception while exiting", exc);
\r
767 Application.Current.Shutdown();
\r
774 private readonly Dictionary<PithosStatus, StatusInfo> _iconNames = new List<StatusInfo>
\r
776 new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"),
\r
777 new StatusInfo(PithosStatus.PollSyncing, "Polling Files", "TraySynching"),
\r
778 new StatusInfo(PithosStatus.LocalSyncing, "Syncing Files", "TraySynching"),
\r
779 new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused")
\r
780 }.ToDictionary(s => s.Status);
\r
782 readonly IWindowManager _windowManager;
\r
784 //private int _syncCount=0;
\r
787 private PithosStatus _pithosStatus = PithosStatus.Disconnected;
\r
789 public void SetPithosStatus(PithosStatus status)
\r
791 if (_pithosStatus == PithosStatus.LocalSyncing && status == PithosStatus.PollComplete)
\r
793 if (_pithosStatus == PithosStatus.PollSyncing && status == PithosStatus.LocalComplete)
\r
795 if (status == PithosStatus.LocalComplete || status == PithosStatus.PollComplete)
\r
796 _pithosStatus = PithosStatus.InSynch;
\r
798 _pithosStatus = status;
\r
802 public void SetPithosStatus(PithosStatus status,string message)
\r
804 StatusMessage = message;
\r
805 SetPithosStatus(status);
\r
808 /* public Notifier GetNotifier(Notification startNotification, Notification endNotification)
\r
810 return new Notifier(this, startNotification, endNotification);
\r
813 public Notifier GetNotifier(string startMessage, string endMessage, params object[] args)
\r
815 return new Notifier(this,
\r
816 new StatusNotification(String.Format(startMessage,args)),
\r
817 new StatusNotification(String.Format(endMessage,args)));
\r
822 /// Updates the visual status indicators of the application depending on status changes, e.g. icon, stat
\r
824 public void UpdateStatus()
\r
827 if (_iconNames.ContainsKey(_pithosStatus))
\r
829 var info = _iconNames[_pithosStatus];
\r
830 StatusIcon = String.Format(@"../Images/{0}.ico", info.IconName);
\r
833 if (_pithosStatus == PithosStatus.InSynch)
\r
834 StatusMessage = "All files up to date";
\r
839 private Task StartMonitor(PithosMonitor monitor,int retries=0)
\r
841 return Task.Factory.StartNew(() =>
\r
843 using (log4net.ThreadContext.Stacks["Monitor"].Push("Start"))
\r
847 Log.InfoFormat("Start Monitoring {0}", monitor.UserName);
\r
851 catch (WebException exc)
\r
853 if (AbandonRetry(monitor, retries))
\r
856 HttpStatusCode statusCode =HttpStatusCode.OK;
\r
857 var response = exc.Response as HttpWebResponse;
\r
859 statusCode = response.StatusCode;
\r
861 switch (statusCode)
\r
863 case HttpStatusCode.Unauthorized:
\r
864 var message = String.Format("API Key Expired for {0}. Starting Renewal",
\r
866 Log.Error(message, exc);
\r
867 var account = Settings.Accounts.Find(acc => acc.AccountKey == new Uri(monitor.AuthenticationUrl).Combine(monitor.UserName));
\r
868 account.IsExpired = true;
\r
869 Notify(new ExpirationNotification(account));
\r
870 //TryAuthorize(monitor.UserName, retries).Wait();
\r
872 case HttpStatusCode.ProxyAuthenticationRequired:
\r
873 TryAuthenticateProxy(monitor,retries);
\r
876 TryLater(monitor, exc, retries);
\r
880 catch (Exception exc)
\r
882 if (AbandonRetry(monitor, retries))
\r
885 TryLater(monitor,exc,retries);
\r
891 private void TryAuthenticateProxy(PithosMonitor monitor,int retries)
\r
893 Execute.OnUIThread(() =>
\r
895 var proxyAccount = IoC.Get<ProxyAccountViewModel>();
\r
896 proxyAccount.Settings = Settings;
\r
897 if (true != _windowManager.ShowDialog(proxyAccount))
\r
899 StartMonitor(monitor, retries);
\r
900 NotifyOfPropertyChange(() => Accounts);
\r
904 private bool AbandonRetry(PithosMonitor monitor, int retries)
\r
908 var message = String.Format("Monitoring of account {0} has failed too many times. Will not retry",
\r
910 _events.Publish(new Notification
\r
911 {Title = "Account monitoring failed", Message = message, Level = TraceLevel.Error});
\r
918 private void TryLater(PithosMonitor monitor, Exception exc,int retries)
\r
920 var message = String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds");
\r
921 Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor,retries+1));
\r
922 _events.Publish(new Notification
\r
923 {Title = "Error", Message = message, Level = TraceLevel.Error});
\r
924 Log.Error(message, exc);
\r
928 public void NotifyChange(string status, TraceLevel level=TraceLevel.Info)
\r
930 StatusMessage = status;
\r
932 _events.Publish(new Notification { Title = "Pithos+", Message = status, Level = level });
\r
935 public void NotifyChangedFile(string filePath)
\r
937 if (RecentFiles.Any(e => e.FullPath == filePath))
\r
940 IProducerConsumerCollection<FileEntry> files=RecentFiles;
\r
942 while (files.Count > 5)
\r
943 files.TryTake(out popped);
\r
944 var entry = new FileEntry { FullPath = filePath };
\r
945 files.TryAdd(entry);
\r
948 public void NotifyAccount(AccountInfo account)
\r
950 if (account== null)
\r
952 //TODO: What happens to an existing account whose Token has changed?
\r
953 account.SiteUri= String.Format("{0}/ui/?token={1}&user={2}",
\r
954 account.SiteUri, Uri.EscapeDataString(account.Token),
\r
955 Uri.EscapeDataString(account.UserName));
\r
957 if (!Accounts.Any(item => item.UserName == account.UserName && item.SiteUri == account.SiteUri))
\r
958 Accounts.TryAdd(account);
\r
962 public void NotifyConflicts(IEnumerable<FileSystemInfo> conflictFiles, string message)
\r
964 if (conflictFiles == null)
\r
966 //Convert to list to avoid multiple iterations
\r
967 var files = conflictFiles.ToList();
\r
968 if (files.Count==0)
\r
972 //TODO: Create a more specific message. For now, just show a warning
\r
973 NotifyForFiles(files,message,TraceLevel.Warning);
\r
977 public void NotifyForFiles(IEnumerable<FileSystemInfo> files, string message,TraceLevel level=TraceLevel.Info)
\r
984 StatusMessage = message;
\r
986 _events.Publish(new Notification { Title = "Pithos+", Message = message, Level = level});
\r
989 public void Notify(Notification notification)
\r
991 TaskEx.Run(()=> _events.Publish(notification));
\r
995 public void RemoveMonitor(string serverUrl,string accountName)
\r
997 if (String.IsNullOrWhiteSpace(accountName))
\r
1000 var accountInfo=_accounts.FirstOrDefault(account => account.UserName == accountName && account.StorageUri.ToString().StartsWith(serverUrl));
\r
1001 if (accountInfo != null)
\r
1003 _accounts.TryRemove(accountInfo);
\r
1004 _pollAgent.RemoveAccount(accountInfo);
\r
1007 var accountKey = new Uri(serverUrl).Combine(accountName);
\r
1008 PithosMonitor monitor;
\r
1009 if (Monitors.TryRemove(accountKey, out monitor))
\r
1012 //TODO: Also remove any pending actions for this account
\r
1013 //from the network queue
\r
1017 public void RefreshOverlays()
\r
1019 foreach (var pair in Monitors)
\r
1021 var monitor = pair.Value;
\r
1023 var path = monitor.RootPath;
\r
1025 if (String.IsNullOrWhiteSpace(path))
\r
1028 if (!Directory.Exists(path) && !File.Exists(path))
\r
1031 IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path);
\r
1035 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM,
\r
1036 HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT,
\r
1037 pathPointer, IntPtr.Zero);
\r
1041 Marshal.FreeHGlobal(pathPointer);
\r
1046 #region Event Handlers
\r
1048 public void Handle(SelectiveSynchChanges message)
\r
1052 PithosMonitor monitor;
\r
1053 if (Monitors.TryGetValue(message.Account.AccountKey, out monitor))
\r
1055 Selectives.SetIsSelectiveEnabled(message.Account.AccountKey, message.Enabled);
\r
1056 monitor.SetSelectivePaths(message.Uris, message.Added, message.Removed);
\r
1059 var account = Accounts.FirstOrDefault(acc => acc.AccountKey == message.Account.AccountKey);
\r
1060 if (account != null)
\r
1062 var added=monitor.UrisToFilePaths(message.Added);
\r
1063 _pollAgent.SynchNow(added);
\r
1070 private bool _pollStarted;
\r
1071 private Sparkle _sparkle;
\r
1072 private bool _manualUpgradeCheck;
\r
1074 //SMELL: Doing so much work for notifications in the shell is wrong
\r
1075 //The notifications should be moved to their own view/viewmodel pair
\r
1076 //and different templates should be used for different message types
\r
1077 //This will also allow the addition of extra functionality, eg. actions
\r
1079 public void Handle(Notification notification)
\r
1083 if (!Settings.ShowDesktopNotifications)
\r
1086 if (notification is PollNotification)
\r
1088 _pollStarted = true;
\r
1091 if (notification is CloudNotification)
\r
1093 if (!_pollStarted)
\r
1095 _pollStarted= false;
\r
1096 notification.Title = "Pithos+";
\r
1097 notification.Message = "Start Synchronisation";
\r
1100 var deleteNotification = notification as CloudDeleteNotification;
\r
1101 if (deleteNotification != null)
\r
1103 StatusMessage = String.Format("Deleted {0}", deleteNotification.Data.Name);
\r
1107 var progress = notification as ProgressNotification;
\r
1110 if (progress != null)
\r
1112 double percentage = (progress.TotalBlocks == progress.Block) ? 1
\r
1113 :(progress.Block + progress.BlockPercentage / 100.0) / (double)progress.TotalBlocks;
\r
1114 StatusMessage = String.Format("{0} {1:p2} of {2} - {3}",
\r
1117 progress.FileSize.ToByteSize(),
\r
1118 progress.FileName);
\r
1122 var info = notification as StatusNotification;
\r
1125 StatusMessage = info.Title;
\r
1128 if (String.IsNullOrWhiteSpace(notification.Message) && String.IsNullOrWhiteSpace(notification.Title))
\r
1131 if (notification.Level <= TraceLevel.Warning)
\r
1132 ShowBalloonFor(notification);
\r
1135 private void ShowBalloonFor(Notification notification)
\r
1137 Contract.Requires(notification!=null);
\r
1139 if (!Settings.ShowDesktopNotifications)
\r
1143 switch (notification.Level)
\r
1145 case TraceLevel.Verbose:
\r
1147 case TraceLevel.Info:
\r
1148 icon = BalloonIcon.Info;
\r
1150 case TraceLevel.Error:
\r
1151 icon = BalloonIcon.Error;
\r
1153 case TraceLevel.Warning:
\r
1154 icon = BalloonIcon.Warning;
\r
1160 var tv = (ShellView) GetView();
\r
1161 System.Action clickAction = null;
\r
1162 if (notification is ExpirationNotification)
\r
1164 clickAction = () => ShowPreferences("AccountTab");
\r
1166 var balloon = new PithosBalloon
\r
1168 Title = notification.Title,
\r
1169 Message = notification.Message,
\r
1171 ClickAction = clickAction
\r
1173 tv.TaskbarView.ShowCustomBalloon(balloon, PopupAnimation.Fade, 4000);
\r
1178 public void Handle(ShowFilePropertiesEvent message)
\r
1180 if (message == null)
\r
1181 throw new ArgumentNullException("message");
\r
1182 if (String.IsNullOrWhiteSpace(message.FileName) )
\r
1183 throw new ArgumentException("message");
\r
1184 Contract.EndContractBlock();
\r
1186 var fileName = message.FileName;
\r
1187 //TODO: Display file properties for non-container folders
\r
1188 if (File.Exists(fileName))
\r
1189 //Retrieve the full name with exact casing. Pithos names are case sensitive
\r
1190 ShowFileProperties(FileInfoExtensions.GetProperFilePathCapitalization(fileName));
\r
1191 else if (Directory.Exists(fileName))
\r
1192 //Retrieve the full name with exact casing. Pithos names are case sensitive
\r
1194 var path = FileInfoExtensions.GetProperDirectoryCapitalization(fileName);
\r
1195 if (IsContainer(path))
\r
1196 ShowContainerProperties(path);
\r
1198 ShowFileProperties(path);
\r
1202 private bool IsContainer(string path)
\r
1204 var matchingFolders = from account in _accounts
\r
1205 from rootFolder in Directory.GetDirectories(account.AccountPath)
\r
1206 where rootFolder.Equals(path, StringComparison.InvariantCultureIgnoreCase)
\r
1207 select rootFolder;
\r
1208 return matchingFolders.Any();
\r
1211 public FileStatus GetFileStatus(string localFileName)
\r
1213 if (String.IsNullOrWhiteSpace(localFileName))
\r
1214 throw new ArgumentNullException("localFileName");
\r
1215 Contract.EndContractBlock();
\r
1217 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1218 var status=statusKeeper.GetFileStatus(localFileName);
\r
1222 public void RemoveAccountFromDatabase(AccountSettings account)
\r
1224 var statusKeeper = IoC.Get<IStatusKeeper>();
\r
1225 statusKeeper.ClearFolderStatus(account.RootPath);
\r