PageRenderTime 28ms CodeModel.GetById 2ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/SparkleShare/SparkleEventLogController.cs

http://github.com/hbons/SparkleShare
C# | 643 lines | 475 code | 152 blank | 16 comment | 93 complexity | df727488b2839886ad554b2d14c70b60 MD5 | raw file
  1//   SparkleShare, a collaboration and sharing tool.
  2//   Copyright (C) 2010  Hylke Bons <hylkebons@gmail.com>
  3//
  4//   This program is free software: you can redistribute it and/or modify
  5//   it under the terms of the GNU General Public License as published by
  6//   the Free Software Foundation, either version 3 of the License, or
  7//   (at your option) any later version.
  8//
  9//   This program is distributed in the hope that it will be useful,
 10//   but WITHOUT ANY WARRANTY; without even the implied warranty of
 11//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12//   GNU General Public License for more details.
 13//
 14//   You should have received a copy of the GNU General Public License
 15//   along with this program. If not, see <http://www.gnu.org/licenses/>.
 16
 17
 18using System;
 19using System.Collections.Generic;
 20using System.Diagnostics;
 21using System.Globalization;
 22using System.IO;
 23using System.Text;
 24using System.Text.RegularExpressions;
 25using System.Threading;
 26
 27using SparkleLib;
 28
 29namespace SparkleShare {
 30
 31    public class SparkleEventLogController {
 32
 33        public event Action ShowWindowEvent = delegate { };
 34        public event Action HideWindowEvent = delegate { };
 35        public event Action ContentLoadingEvent = delegate { };
 36
 37        public event UpdateContentEventEventHandler UpdateContentEvent = delegate { };
 38        public delegate void UpdateContentEventEventHandler (string html);
 39        
 40        public event UpdateChooserEventHandler UpdateChooserEvent = delegate { };
 41        public delegate void UpdateChooserEventHandler (string [] folders);
 42        
 43        public event UpdateChooserEnablementEventHandler UpdateChooserEnablementEvent = delegate { };
 44        public delegate void UpdateChooserEnablementEventHandler (bool enabled);
 45        
 46        public event UpdateSizeInfoEventHandler UpdateSizeInfoEvent = delegate { };
 47        public delegate void UpdateSizeInfoEventHandler (string size, string history_size);
 48        
 49        public event ShowSaveDialogEventHandler ShowSaveDialogEvent = delegate { };
 50        public delegate void ShowSaveDialogEventHandler (string file_name, string target_folder_path);
 51
 52
 53        private string selected_folder;
 54        private RevisionInfo restore_revision_info;
 55        private bool history_view_active;
 56
 57
 58        public bool WindowIsOpen { get; private set; }
 59
 60        public string SelectedFolder {
 61            get {
 62                return this.selected_folder;
 63            }
 64
 65            set {
 66                this.selected_folder = value;
 67
 68                ContentLoadingEvent ();
 69                UpdateSizeInfoEvent ("…", "…");
 70
 71                new Thread (() => {
 72                    SparkleDelay delay = new SparkleDelay ();
 73                    string html = HTML;
 74                    delay.Stop ();
 75
 76					if (!string.IsNullOrEmpty (html))
 77                    	UpdateContentEvent (html);
 78
 79                    UpdateSizeInfoEvent (Size, HistorySize);
 80
 81                }).Start ();
 82            }
 83        }
 84
 85        public string HTML {
 86            get {
 87                List<SparkleChangeSet> change_sets = GetLog (this.selected_folder);
 88                string html = GetHTMLLog (change_sets);
 89
 90                return html;
 91            }
 92        }
 93
 94        public string [] Folders {
 95            get {
 96                return Program.Controller.Folders.ToArray ();
 97            }
 98        }
 99
100        public string Size {
101            get {
102                double size = 0;
103
104                foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
105                    if (this.selected_folder == null) {
106                        size += repo.Size;
107
108                    } else if (this.selected_folder.Equals (repo.Name)) {
109                        if (repo.Size == 0)
110                            return "???";
111                        else
112                            return repo.Size.ToSize ();
113                    }
114                }
115
116                if (size == 0)
117                    return "???";
118                else
119                    return size.ToSize ();
120            }
121        }
122
123        public string HistorySize {
124            get {
125                double size = 0;
126
127                foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
128                    if (this.selected_folder == null) {
129                        size += repo.HistorySize;
130
131                    } else if (this.selected_folder.Equals (repo.Name)) {
132                        if (repo.HistorySize == 0)
133                            return "???";
134                        else
135                            return repo.HistorySize.ToSize ();
136                    }
137                }
138
139                if (size == 0)
140                    return "???";
141                else
142                    return size.ToSize ();
143            }
144        }
145
146
147        public SparkleEventLogController ()
148        {
149            Program.Controller.ShowEventLogWindowEvent += delegate {
150                if (!WindowIsOpen) {
151                    ContentLoadingEvent ();
152                    UpdateSizeInfoEvent ("…", "…");
153
154                    if (this.selected_folder == null) {
155                        new Thread (() => {
156                            SparkleDelay delay = new SparkleDelay ();
157                            string html = HTML;
158                            delay.Stop ();
159
160                            UpdateChooserEvent (Folders);
161                            UpdateChooserEnablementEvent (true);
162
163							if (!string.IsNullOrEmpty (html))
164                            	UpdateContentEvent (html);
165                            
166							UpdateSizeInfoEvent (Size, HistorySize);
167
168                        }).Start ();
169                    }
170                }
171
172                WindowIsOpen = true;
173                ShowWindowEvent ();
174            };
175			
176            Program.Controller.OnIdle += delegate {
177                if (this.history_view_active)
178                    return;
179
180                ContentLoadingEvent ();
181                UpdateSizeInfoEvent ("…", "…");
182
183                SparkleDelay delay = new SparkleDelay ();
184                string html = HTML;
185                delay.Stop ();
186
187				if (!string.IsNullOrEmpty (html))
188                	UpdateContentEvent (html);
189
190                UpdateSizeInfoEvent (Size, HistorySize);
191            };
192			
193            Program.Controller.FolderListChanged += delegate {
194                if (this.selected_folder != null && !Program.Controller.Folders.Contains (this.selected_folder))
195                    this.selected_folder = null;
196
197                UpdateChooserEvent (Folders);
198                UpdateSizeInfoEvent (Size, HistorySize);
199            };
200        }
201
202
203        public void WindowClosed ()
204        {
205            WindowIsOpen = false;
206            HideWindowEvent ();
207			this.selected_folder = null;
208        }
209
210
211        public void LinkClicked (string url)
212        {
213            if (url.StartsWith ("about:") || string.IsNullOrEmpty (url))
214                return;
215				
216            url = url.Replace ("%20", " ");
217            
218            if (url.StartsWith ("http")) {
219                Program.Controller.OpenWebsite (url);
220            
221            } else if (url.StartsWith ("restore://") && this.restore_revision_info == null) {
222                Regex regex = new Regex ("restore://(.+)/([a-f0-9]+)/(.+)/(.{3} [0-9]+ [0-9]+h[0-9]+)/(.+)");
223                Match match = regex.Match (url);
224                
225                if (match.Success) {
226                    string author_name = match.Groups [3].Value;
227                    string timestamp   = match.Groups [4].Value;
228
229                    this.restore_revision_info = new RevisionInfo () {
230                        Folder   = new SparkleFolder (match.Groups [1].Value),
231                        Revision = match.Groups [2].Value,
232                        FilePath = Uri.UnescapeDataString (match.Groups [5].Value)
233                    };
234
235                    string file_name = Path.GetFileNameWithoutExtension (this.restore_revision_info.FilePath) +
236                        " (" + author_name + " " + timestamp + ")" + Path.GetExtension (this.restore_revision_info.FilePath);
237
238                    string target_folder_path = Path.Combine (this.restore_revision_info.Folder.FullPath,
239                        Path.GetDirectoryName (this.restore_revision_info.FilePath));
240
241                    ShowSaveDialogEvent (file_name, target_folder_path);
242                }
243                
244            } else if (url.StartsWith ("back://")) {
245                this.history_view_active = false;
246                SelectedFolder           = this.selected_folder; // TODO: Return to the same position on the page
247
248                UpdateChooserEnablementEvent (true);
249
250            } else if (url.StartsWith ("history://")) {
251                this.history_view_active = true;
252
253                ContentLoadingEvent ();
254                UpdateSizeInfoEvent ("…", "…");
255                UpdateChooserEnablementEvent (false);
256
257                string folder    = url.Replace ("history://", "").Split ("/".ToCharArray ()) [0];
258                string file_path = url.Replace ("history://" + folder + "/", "");
259
260                byte [] file_path_bytes = Encoding.Default.GetBytes (file_path);
261                file_path               = Encoding.UTF8.GetString (file_path_bytes);
262
263                file_path = Uri.UnescapeDataString (file_path);
264
265                foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
266                    if (!repo.Name.Equals (folder))
267						continue;
268
269			        new Thread (() => {
270                        SparkleDelay delay = new SparkleDelay ();
271                        List<SparkleChangeSet> change_sets = repo.GetChangeSets (file_path);
272                        string html = GetHistoryHTMLLog (change_sets, file_path);
273                        delay.Stop ();
274
275						if (!string.IsNullOrEmpty (html))
276	                    	UpdateContentEvent (html);
277
278                	}).Start ();
279
280                    break;
281                }
282
283            } else {
284                Program.Controller.OpenFile (url);
285            }   
286        }
287
288
289        public void SaveDialogCompleted (string target_file_path)
290        {
291            foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
292                if (repo.Name.Equals (this.restore_revision_info.Folder.Name)) {
293                    repo.RestoreFile (this.restore_revision_info.FilePath,
294                        this.restore_revision_info.Revision, target_file_path);
295
296                    break;
297                }
298            }
299
300            this.restore_revision_info = null;
301            Program.Controller.OpenFolder (Path.GetDirectoryName (target_file_path));
302        }
303
304
305        public void SaveDialogCancelled ()
306        {
307            this.restore_revision_info = null;
308        }
309
310
311        private List<SparkleChangeSet> GetLog ()
312        {
313            List<SparkleChangeSet> list = new List<SparkleChangeSet> ();
314
315            foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
316                List<SparkleChangeSet> change_sets = repo.ChangeSets;
317
318                if (change_sets != null)
319                    list.AddRange (change_sets);
320                else
321                    SparkleLogger.LogInfo ("Log", "Could not create log for " + repo.Name);
322            }
323
324            list.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp)));
325            list.Reverse ();
326
327            if (list.Count > 100)
328                return list.GetRange (0, 100);
329            else
330                return list.GetRange (0, list.Count);
331        }
332
333
334        private List<SparkleChangeSet> GetLog (string name)
335        {
336            if (name == null)
337                return GetLog ();
338
339            foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
340                if (repo.Name.Equals (name)) {
341                    List<SparkleChangeSet> change_sets = repo.ChangeSets;
342
343                    if (change_sets != null)
344                        return change_sets;
345                    else
346                        break;
347                }
348            }
349
350            return new List<SparkleChangeSet> ();
351        }
352
353
354        public string GetHistoryHTMLLog (List<SparkleChangeSet> change_sets, string file_path)
355        {
356            string html = "<div class='history-header'>" +
357                "<a class='windows' href='back://'>&laquo; Back</a> &nbsp;|&nbsp; ";
358
359            if (change_sets.Count > 1)
360                html += "Revisions for <b>&ldquo;";
361            else
362                html += "No revisions for <b>&ldquo;";
363
364            html += Path.GetFileName (file_path) + "&rdquo;</b>";
365			html += "</div><div class='table-wrapper'><table>";
366
367            if (change_sets.Count > 0)
368                change_sets.RemoveAt (0);
369
370            foreach (SparkleChangeSet change_set in change_sets) {
371                html += "<tr>" +
372                            "<td class='avatar'><img src='" + GetAvatarFilePath (change_set.User) + "'></td>" +
373                            "<td class='name'>" + change_set.User.Name + "</td>" +
374                            "<td class='date'>" + 
375                                change_set.Timestamp.ToString ("d MMM yyyy", CultureInfo.InvariantCulture) + 
376                            "</td>" +
377                            "<td class='time'>" + change_set.Timestamp.ToString ("HH:mm") + "</td>" +
378                            "<td class='restore'>" +
379                                "<a href='restore://" + change_set.Folder.Name + "/" + 
380                                change_set.Revision + "/" + change_set.User.Name + "/" + 
381                                change_set.Timestamp.ToString ("MMM d H\\hmm", CultureInfo.InvariantCulture) + "/" +
382                                file_path + "'>Restore&hellip;</a>" +
383                            "</td>" +
384                        "</tr>";
385            }
386
387            html += "</table></div>";
388            html = Program.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->", html);
389
390			return html.Replace ("<!-- $midnight -->", "100000000");
391        }
392
393
394        public string GetHTMLLog (List<SparkleChangeSet> change_sets)
395        {
396            if (change_sets == null || change_sets.Count == 0)
397                return Program.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->",
398                    "<div class='day-entry'><div class='day-entry-header'>This project does not keep a history.</div></div>");
399
400            List <ActivityDay> activity_days = new List <ActivityDay> ();
401
402            change_sets.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp)));
403            change_sets.Reverse ();
404
405            foreach (SparkleChangeSet change_set in change_sets) {
406                bool change_set_inserted = false;
407            
408                foreach (ActivityDay stored_activity_day in activity_days) {
409                    if (stored_activity_day.Date.Year  == change_set.Timestamp.Year &&
410                        stored_activity_day.Date.Month == change_set.Timestamp.Month &&
411                        stored_activity_day.Date.Day   == change_set.Timestamp.Day) {
412
413                        stored_activity_day.Add (change_set);
414
415                        change_set_inserted = true;
416                        break;
417                    }
418                }
419
420                if (!change_set_inserted) {
421                    ActivityDay activity_day = new ActivityDay (change_set.Timestamp);
422                    activity_day.Add (change_set);
423                    activity_days.Add (activity_day);
424                }
425            }
426
427            string event_log_html   = Program.Controller.EventLogHTML;
428            string day_entry_html   = Program.Controller.DayEntryHTML;
429            string event_entry_html = Program.Controller.EventEntryHTML;
430            string event_log        = "";
431
432            foreach (ActivityDay activity_day in activity_days) {
433                string event_entries = "";
434
435                foreach (SparkleChangeSet change_set in activity_day) {
436                    string event_entry = "<dl>";
437
438                    foreach (SparkleChange change in change_set.Changes) {
439                        if (change.Type != SparkleChangeType.Moved) {
440                            event_entry += "<dd class='" + change.Type.ToString ().ToLower () + "'>";
441
442                            if (!change.IsFolder) {
443                                event_entry += "<small><a href=\"history://" + change_set.Folder.Name + "/" + 
444                                    change.Path + "\" title=\"View revisions\">" + change.Timestamp.ToString ("HH:mm") +
445                                    " &#x25BE;</a></small> &nbsp;";
446
447                            } else {
448                                event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") + "</small> &nbsp;";
449                            }
450
451                            event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path);
452                            event_entry += "</dd>";
453
454                        } else {
455                            event_entry += "<dd class='moved'>";
456                            event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> &nbsp;";
457                            event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path);
458                            event_entry += "<br>";
459                            event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> &nbsp;";
460                            event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.MovedToPath);
461                            event_entry += "</dd>";
462                        }
463                    }
464
465                    event_entry += "</dl>";
466
467                    string timestamp = change_set.Timestamp.ToString ("H:mm");
468
469                    if (!change_set.FirstTimestamp.Equals (new DateTime ()) &&
470                        !change_set.Timestamp.ToString ("H:mm").Equals (change_set.FirstTimestamp.ToString ("H:mm"))) {
471
472                        timestamp = change_set.FirstTimestamp.ToString ("H:mm") + " – " + timestamp;
473                    }
474
475                    // TODO: List commit messages if there are any
476                    event_entries += event_entry_html.Replace ("<!-- $event-entry-content -->", event_entry)
477                        .Replace ("<!-- $event-user-name -->", change_set.User.Name)
478                        .Replace ("<!-- $event-user-email -->", change_set.User.Email)
479                        .Replace ("<!-- $event-avatar-url -->", GetAvatarFilePath (change_set.User))
480                        .Replace ("<!-- $event-url -->", change_set.RemoteUrl.ToString ())
481                        .Replace ("<!-- $event-revision -->", change_set.Revision);
482
483                    if (this.selected_folder == null) {
484                        event_entries = event_entries.Replace ("<!-- $event-folder -->", " @ " + change_set.Folder.Name);
485                        event_entries = event_entries.Replace ("<!-- $event-folder-url -->", change_set.Folder.FullPath);
486                    }
487                }
488
489                string day_entry   = "";
490                DateTime today     = DateTime.Now;
491                DateTime yesterday = DateTime.Now.AddDays (-1);
492
493                if (today.Day   == activity_day.Date.Day &&
494                    today.Month == activity_day.Date.Month &&
495                    today.Year  == activity_day.Date.Year) {
496
497                    day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
498                        "<span id='today' name='" +
499                         activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" + "Today" +
500                        "</span>");
501
502                } else if (yesterday.Day   == activity_day.Date.Day &&
503                           yesterday.Month == activity_day.Date.Month &&
504                           yesterday.Year  == activity_day.Date.Year) {
505
506                    day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
507                        "<span id='yesterday' name='" + activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" +
508                        "Yesterday" +
509                        "</span>");
510
511                } else {
512                    if (activity_day.Date.Year != DateTime.Now.Year) {
513                        day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
514                            activity_day.Date.ToString ("dddd, MMMM d, yyyy", CultureInfo.InvariantCulture));
515
516                    } else {
517                        day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
518                            activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture));
519                    }
520                }
521
522                event_log += day_entry.Replace ("<!-- $day-entry-content -->", event_entries);
523            }
524
525            int midnight = (int) (DateTime.Today.AddDays (1) - new DateTime (1970, 1, 1)).TotalSeconds;
526
527            string html = event_log_html.Replace ("<!-- $event-log-content -->", event_log);
528            html = html.Replace ("<!-- $midnight -->", midnight.ToString ());
529
530            return html;
531        }
532
533
534        private string FormatBreadCrumbs (string path_root, string path)
535        {
536            byte [] path_root_bytes = Encoding.Default.GetBytes (path_root);
537            byte [] path_bytes      = Encoding.Default.GetBytes (path);
538            path_root               = Encoding.UTF8.GetString (path_root_bytes);
539            path                    = Encoding.UTF8.GetString (path_bytes);
540
541            path_root                = path_root.Replace ("/", Path.DirectorySeparatorChar.ToString ());
542            path                     = path.Replace ("/", Path.DirectorySeparatorChar.ToString ());
543            string new_path_root     = path_root;
544            string [] crumbs         = path.Split (Path.DirectorySeparatorChar);
545            string link              = "";
546            bool previous_was_folder = false;
547
548            int i = 0;
549            foreach (string crumb in crumbs) {
550                if (string.IsNullOrEmpty (crumb))
551                    continue;
552
553                string crumb_path = SafeCombine (new_path_root, crumb);
554
555                if (Directory.Exists (crumb_path)) {
556                    link += "<a href='" + crumb_path + "'>" + crumb + Path.DirectorySeparatorChar + "</a>";
557                    previous_was_folder = true;
558
559                } else if (File.Exists (crumb_path)) {
560                    link += "<a href='" + crumb_path + "'>" + crumb + "</a>";
561                    previous_was_folder = false;
562
563                } else {
564                    if (i > 0 && !previous_was_folder)
565                        link += Path.DirectorySeparatorChar;
566
567                    link += crumb;
568                    previous_was_folder = false;
569                }
570
571                new_path_root = SafeCombine (new_path_root, crumb);
572                i++;
573            }
574
575            return link;
576        }
577
578
579        private string SafeCombine (string path1, string path2)
580        {
581            string result = path1;
582            
583            if (!result.EndsWith (Path.DirectorySeparatorChar.ToString ()))
584                result += Path.DirectorySeparatorChar;
585
586            if (path2.StartsWith (Path.DirectorySeparatorChar.ToString ()))
587                path2 = path2.Substring (1);
588
589            return result + path2;
590        }
591
592
593        private string GetAvatarFilePath (SparkleUser user)
594        {
595            if (!Program.Controller.AvatarsEnabled)
596                return "<!-- $pixmaps-path -->/user-icon-default.png";
597              
598            string fetched_avatar = SparkleAvatars.GetAvatar (user.Email, 48, Program.Controller.Config.FullPath);
599                
600            if (!string.IsNullOrEmpty (fetched_avatar))
601                return "file://" + fetched_avatar.Replace ("\\", "/");
602            else
603                return "<!-- $pixmaps-path -->/user-icon-default.png";
604        }
605
606
607        // All change sets that happened on a day
608        private class ActivityDay : List<SparkleChangeSet>
609        {
610            public DateTime Date;
611
612            public ActivityDay (DateTime date_time)
613            {
614                Date = new DateTime (date_time.Year, date_time.Month, date_time.Day);
615            }
616        }
617
618
619        private class RevisionInfo {
620            public SparkleFolder Folder;
621            public string FilePath;
622            public string Revision;
623        }
624
625
626        private class SparkleDelay : Stopwatch {
627
628            public SparkleDelay () : base ()
629            {
630                Start ();
631            }
632
633
634            new public void Stop ()
635            {
636                base.Stop ();
637
638                if (ElapsedMilliseconds < 500)
639                    Thread.Sleep (500 - (int) ElapsedMilliseconds);
640            }
641        }
642    }
643}