diff options
Diffstat (limited to 'SparkleShare/Common/EventLogController.cs')
-rw-r--r-- | SparkleShare/Common/EventLogController.cs | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/SparkleShare/Common/EventLogController.cs b/SparkleShare/Common/EventLogController.cs new file mode 100644 index 0000000..78831af --- /dev/null +++ b/SparkleShare/Common/EventLogController.cs @@ -0,0 +1,646 @@ +// SparkleShare, a collaboration and sharing tool. +// Copyright (C) 2010 Hylke Bons <hi@planetpeanut.uk> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +using Sparkles; + +namespace SparkleShare { + + public class EventLogController { + + public event Action ShowWindowEvent = delegate { }; + public event Action HideWindowEvent = delegate { }; + public event Action ContentLoadingEvent = delegate { }; + + public event UpdateContentEventEventHandler UpdateContentEvent = delegate { }; + public delegate void UpdateContentEventEventHandler (string html); + + public event UpdateChooserEventHandler UpdateChooserEvent = delegate { }; + public delegate void UpdateChooserEventHandler (string [] folders); + + public event UpdateChooserEnablementEventHandler UpdateChooserEnablementEvent = delegate { }; + public delegate void UpdateChooserEnablementEventHandler (bool enabled); + + public event UpdateSizeInfoEventHandler UpdateSizeInfoEvent = delegate { }; + public delegate void UpdateSizeInfoEventHandler (string size, string history_size); + + public event ShowSaveDialogEventHandler ShowSaveDialogEvent = delegate { }; + public delegate void ShowSaveDialogEventHandler (string file_name, string target_folder_path); + + + private string selected_folder; + private RevisionInfo restore_revision_info; + private bool history_view_active; + + + public bool WindowIsOpen { get; private set; } + + public string SelectedFolder { + get { + return this.selected_folder; + } + + set { + this.selected_folder = value; + + ContentLoadingEvent (); + UpdateSizeInfoEvent ("…", "…"); + + new Thread (() => { + SparkleDelay delay = new SparkleDelay (); + string html = HTML; + delay.Stop (); + + if (!string.IsNullOrEmpty (html)) + UpdateContentEvent (html); + + UpdateSizeInfoEvent (Size, HistorySize); + + }).Start (); + } + } + + public string HTML { + get { + List<ChangeSet> change_sets = GetLog (this.selected_folder); + string html = GetHTMLLog (change_sets); + + return html; + } + } + + public string [] Folders { + get { + return SparkleShare.Controller.Folders.ToArray (); + } + } + + public string Size { + get { + double size = 0; + + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + if (this.selected_folder == null) { + size += repo.Size; + + } else if (this.selected_folder.Equals (repo.Name)) { + if (repo.Size == 0) + return "???"; + else + return repo.Size.ToSize (); + } + } + + if (size == 0) + return "???"; + else + return size.ToSize (); + } + } + + public string HistorySize { + get { + double size = 0; + + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + if (this.selected_folder == null) { + size += repo.HistorySize; + + } else if (this.selected_folder.Equals (repo.Name)) { + if (repo.HistorySize == 0) + return "???"; + else + return repo.HistorySize.ToSize (); + } + } + + if (size == 0) + return "???"; + else + return size.ToSize (); + } + } + + + public EventLogController () + { + SparkleShare.Controller.ShowEventLogWindowEvent += delegate { + if (!WindowIsOpen) { + ContentLoadingEvent (); + UpdateSizeInfoEvent ("…", "…"); + + if (this.selected_folder == null) { + new Thread (() => { + SparkleDelay delay = new SparkleDelay (); + string html = HTML; + delay.Stop (); + + UpdateChooserEvent (Folders); + UpdateChooserEnablementEvent (true); + + if (!string.IsNullOrEmpty (html)) + UpdateContentEvent (html); + + UpdateSizeInfoEvent (Size, HistorySize); + + }).Start (); + } + } + + WindowIsOpen = true; + ShowWindowEvent (); + }; + + SparkleShare.Controller.OnIdle += delegate { + if (this.history_view_active) + return; + + ContentLoadingEvent (); + UpdateSizeInfoEvent ("…", "…"); + + SparkleDelay delay = new SparkleDelay (); + string html = HTML; + delay.Stop (); + + if (!string.IsNullOrEmpty (html)) + UpdateContentEvent (html); + + UpdateSizeInfoEvent (Size, HistorySize); + }; + + SparkleShare.Controller.FolderListChanged += delegate { + if (this.selected_folder != null && !SparkleShare.Controller.Folders.Contains (this.selected_folder)) + this.selected_folder = null; + + UpdateChooserEvent (Folders); + UpdateSizeInfoEvent (Size, HistorySize); + }; + } + + + public void WindowClosed () + { + WindowIsOpen = false; + HideWindowEvent (); + this.selected_folder = null; + } + + + public void LinkClicked (string href) + { + if (string.IsNullOrEmpty (href) || href.StartsWith ("about:")) + return; + + href = href.Replace ("%20", " "); + + if (href.StartsWith ("http")) { + SparkleShare.Controller.OpenWebsite (href); + + } else if (href.StartsWith ("restore://") && this.restore_revision_info == null) { + Regex regex = new Regex ("restore://(.+)/([a-f0-9]+)/(.+)/(.{3} [0-9]+ [0-9]+h[0-9]+)/(.+)"); + Match match = regex.Match (href); + + if (match.Success) { + string author_name = match.Groups [3].Value; + string timestamp = match.Groups [4].Value; + + this.restore_revision_info = new RevisionInfo () { + Folder = new SparkleFolder (match.Groups [1].Value), + Revision = match.Groups [2].Value, + FilePath = Uri.UnescapeDataString (match.Groups [5].Value) + }; + + string file_name = Path.GetFileNameWithoutExtension (this.restore_revision_info.FilePath) + + " (" + author_name + " " + timestamp + ")" + Path.GetExtension (this.restore_revision_info.FilePath); + + string target_folder_path = Path.Combine (this.restore_revision_info.Folder.FullPath, + Path.GetDirectoryName (this.restore_revision_info.FilePath)); + + ShowSaveDialogEvent (file_name, target_folder_path); + } + + } else if (href.StartsWith ("back://")) { + this.history_view_active = false; + SelectedFolder = this.selected_folder; // TODO: Return to the same position on the page + + UpdateChooserEnablementEvent (true); + + } else if (href.StartsWith ("history://")) { + this.history_view_active = true; + + ContentLoadingEvent (); + UpdateSizeInfoEvent ("…", "…"); + UpdateChooserEnablementEvent (false); + + string folder = href.Replace ("history://", "").Split ("/".ToCharArray ()) [0]; + string file_path = href.Replace ("history://" + folder + "/", ""); + + byte [] file_path_bytes = Encoding.Default.GetBytes (file_path); + file_path = Encoding.UTF8.GetString (file_path_bytes); + + file_path = Uri.UnescapeDataString (file_path); + + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + if (!repo.Name.Equals (folder)) + continue; + + new Thread (() => { + SparkleDelay delay = new SparkleDelay (); + List<ChangeSet> change_sets = repo.GetChangeSets (file_path); + string html = GetHistoryHTMLLog (change_sets, file_path); + delay.Stop (); + + if (!string.IsNullOrEmpty (html)) + UpdateContentEvent (html); + + }).Start (); + + break; + } + + } else { + if (href.StartsWith ("file:///")) + href = href.Substring (7); + + SparkleShare.Controller.OpenFile (href); + } + } + + + public void SaveDialogCompleted (string target_file_path) + { + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + if (repo.Name.Equals (this.restore_revision_info.Folder.Name)) { + repo.RestoreFile (this.restore_revision_info.FilePath, + this.restore_revision_info.Revision, target_file_path); + + break; + } + } + + this.restore_revision_info = null; + SparkleShare.Controller.OpenFolder (Path.GetDirectoryName (target_file_path)); + } + + + public void SaveDialogCancelled () + { + this.restore_revision_info = null; + } + + + private List<ChangeSet> GetLog () + { + List<ChangeSet> list = new List<ChangeSet> (); + + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + List<ChangeSet> change_sets = repo.ChangeSets; + + if (change_sets != null) + list.AddRange (change_sets); + else + Logger.LogInfo ("Log", "Could not create log for " + repo.Name); + } + + list.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp))); + list.Reverse (); + + if (list.Count > 100) + return list.GetRange (0, 100); + else + return list.GetRange (0, list.Count); + } + + + private List<ChangeSet> GetLog (string name) + { + if (name == null) + return GetLog (); + + foreach (BaseRepository repo in SparkleShare.Controller.Repositories) { + if (repo.Name.Equals (name)) { + List<ChangeSet> change_sets = repo.ChangeSets; + + if (change_sets != null) + return change_sets; + else + break; + } + } + + return new List<ChangeSet> (); + } + + + public string GetHistoryHTMLLog (List<ChangeSet> change_sets, string file_path) + { + string html = "<div class='history-header'>" + + "<a class='windows' href='back://'>« Back</a> | "; + + if (change_sets.Count > 1) + html += "Revisions for <b>“"; + else + html += "No revisions for <b>“"; + + html += Path.GetFileName (file_path) + "”</b>"; + html += "</div><div class='table-wrapper'><table>"; + + if (change_sets.Count > 0) + change_sets.RemoveAt (0); + + foreach (ChangeSet change_set in change_sets) { + html += "<tr>" + + "<td class='avatar'><img src='" + GetAvatarFilePath (change_set.User) + "'></td>" + + "<td class='name'>" + change_set.User.Name + "</td>" + + "<td class='date'>" + + change_set.Timestamp.ToString ("d MMM yyyy", CultureInfo.InvariantCulture) + + "</td>" + + "<td class='time'>" + change_set.Timestamp.ToString ("HH:mm") + "</td>" + + "<td class='restore'>" + + "<a href='restore://" + change_set.Folder.Name + "/" + + change_set.Revision + "/" + change_set.User.Name + "/" + + change_set.Timestamp.ToString ("MMM d H\\hmm", CultureInfo.InvariantCulture) + "/" + + file_path + "'>Restore…</a>" + + "</td>" + + "</tr>"; + } + + html += "</table></div>"; + html = SparkleShare.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->", html); + + return html.Replace ("<!-- $midnight -->", "100000000"); + } + + + public string GetHTMLLog (List<ChangeSet> change_sets) + { + if (change_sets == null || change_sets.Count == 0) + return SparkleShare.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->", + "<div class='day-entry'><div class='day-entry-header'>This project does not keep a history.</div></div>"); + + List <ActivityDay> activity_days = new List <ActivityDay> (); + + change_sets.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp))); + change_sets.Reverse (); + + foreach (ChangeSet change_set in change_sets) { + bool change_set_inserted = false; + + foreach (ActivityDay stored_activity_day in activity_days) { + if (stored_activity_day.Date.Year == change_set.Timestamp.Year && + stored_activity_day.Date.Month == change_set.Timestamp.Month && + stored_activity_day.Date.Day == change_set.Timestamp.Day) { + + stored_activity_day.Add (change_set); + + change_set_inserted = true; + break; + } + } + + if (!change_set_inserted) { + ActivityDay activity_day = new ActivityDay (change_set.Timestamp); + activity_day.Add (change_set); + activity_days.Add (activity_day); + } + } + + string event_log_html = SparkleShare.Controller.EventLogHTML; + string day_entry_html = SparkleShare.Controller.DayEntryHTML; + string event_entry_html = SparkleShare.Controller.EventEntryHTML; + string event_log = ""; + + foreach (ActivityDay activity_day in activity_days) { + string event_entries = ""; + + foreach (ChangeSet change_set in activity_day) { + string event_entry = "<dl>"; + + foreach (Change change in change_set.Changes) { + if (change.Type != ChangeType.Moved) { + event_entry += "<dd class='" + change.Type.ToString ().ToLower () + "'>"; + + if (!change.IsFolder) { + event_entry += "<small><a href=\"history://" + change_set.Folder.Name + "/" + + change.Path + "\" title=\"View revisions\">" + change.Timestamp.ToString ("HH:mm") + + " ▾</a></small> "; + + } else { + event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") + "</small> "; + } + + event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path); + event_entry += "</dd>"; + + } else { + event_entry += "<dd class='moved'>"; + event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> "; + event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path); + event_entry += "<br>"; + event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> "; + event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.MovedToPath); + event_entry += "</dd>"; + } + } + + event_entry += "</dl>"; + + string timestamp = change_set.Timestamp.ToString ("H:mm"); + + if (!change_set.FirstTimestamp.Equals (new DateTime ()) && + !change_set.Timestamp.ToString ("H:mm").Equals (change_set.FirstTimestamp.ToString ("H:mm"))) { + + timestamp = change_set.FirstTimestamp.ToString ("H:mm") + " – " + timestamp; + } + + // TODO: List commit messages if there are any + event_entries += event_entry_html.Replace ("<!-- $event-entry-content -->", event_entry) + .Replace ("<!-- $event-user-name -->", change_set.User.Name) + .Replace ("<!-- $event-user-email -->", change_set.User.Email) + .Replace ("<!-- $event-avatar-url -->", GetAvatarFilePath (change_set.User)) + .Replace ("<!-- $event-url -->", change_set.RemoteUrl.ToString ()) + .Replace ("<!-- $event-revision -->", change_set.Revision); + + if (this.selected_folder == null) { + event_entries = event_entries.Replace ("<!-- $event-folder -->", " @ " + change_set.Folder.Name); + event_entries = event_entries.Replace ("<!-- $event-folder-url -->", change_set.Folder.FullPath); + } + } + + string day_entry = ""; + DateTime today = DateTime.Now; + DateTime yesterday = DateTime.Now.AddDays (-1); + + if (today.Day == activity_day.Date.Day && + today.Month == activity_day.Date.Month && + today.Year == activity_day.Date.Year) { + + day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->", + "<span id='today' name='" + + activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" + "Today" + + "</span>"); + + } else if (yesterday.Day == activity_day.Date.Day && + yesterday.Month == activity_day.Date.Month && + yesterday.Year == activity_day.Date.Year) { + + day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->", + "<span id='yesterday' name='" + activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" + + "Yesterday" + + "</span>"); + + } else { + if (activity_day.Date.Year != DateTime.Now.Year) { + day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->", + activity_day.Date.ToString ("dddd, MMMM d, yyyy", CultureInfo.InvariantCulture)); + + } else { + day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->", + activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture)); + } + } + + event_log += day_entry.Replace ("<!-- $day-entry-content -->", event_entries); + } + + int midnight = (int) (DateTime.Today.AddDays (1) - new DateTime (1970, 1, 1)).TotalSeconds; + + string html = event_log_html.Replace ("<!-- $event-log-content -->", event_log); + html = html.Replace ("<!-- $midnight -->", midnight.ToString ()); + + return html; + } + + + private string FormatBreadCrumbs (string path_root, string path) + { + byte [] path_root_bytes = Encoding.Default.GetBytes (path_root); + byte [] path_bytes = Encoding.Default.GetBytes (path); + path_root = Encoding.UTF8.GetString (path_root_bytes); + path = Encoding.UTF8.GetString (path_bytes); + + path_root = path_root.Replace ("/", Path.DirectorySeparatorChar.ToString ()); + path = path.Replace ("/", Path.DirectorySeparatorChar.ToString ()); + string new_path_root = path_root; + string [] crumbs = path.Split (Path.DirectorySeparatorChar); + string link = ""; + bool previous_was_folder = false; + + int i = 0; + foreach (string crumb in crumbs) { + if (string.IsNullOrEmpty (crumb)) + continue; + + string crumb_path = SafeCombine (new_path_root, crumb); + + if (Directory.Exists (crumb_path)) { + link += "<a href='" + crumb_path + "'>" + crumb + Path.DirectorySeparatorChar + "</a>"; + previous_was_folder = true; + + } else if (File.Exists (crumb_path)) { + link += "<a href='" + crumb_path + "'>" + crumb + "</a>"; + previous_was_folder = false; + + } else { + if (i > 0 && !previous_was_folder) + link += Path.DirectorySeparatorChar; + + link += crumb; + previous_was_folder = false; + } + + new_path_root = SafeCombine (new_path_root, crumb); + i++; + } + + return link; + } + + + private string SafeCombine (string path1, string path2) + { + string result = path1; + + if (!result.EndsWith (Path.DirectorySeparatorChar.ToString ())) + result += Path.DirectorySeparatorChar; + + if (path2.StartsWith (Path.DirectorySeparatorChar.ToString ())) + path2 = path2.Substring (1); + + return result + path2; + } + + + private string GetAvatarFilePath (User user) + { + if (!SparkleShare.Controller.AvatarsEnabled) + return "<!-- $pixmaps-path -->/user-icon-default.png"; + + string fetched_avatar = Avatars.GetAvatar (user.Email, 48, SparkleShare.Controller.Config.DirectoryPath); + + if (!string.IsNullOrEmpty (fetched_avatar)) + return "file://" + fetched_avatar.Replace ("\\", "/"); + else + return "<!-- $pixmaps-path -->/user-icon-default.png"; + } + + + // All change sets that happened on a day + private class ActivityDay : List<ChangeSet> + { + public DateTime Date; + + public ActivityDay (DateTime date_time) + { + Date = new DateTime (date_time.Year, date_time.Month, date_time.Day); + } + } + + + private class RevisionInfo { + public SparkleFolder Folder; + public string FilePath; + public string Revision; + } + + + private class SparkleDelay : Stopwatch { + + public SparkleDelay () : base () + { + Start (); + } + + + new public void Stop () + { + base.Stop (); + + if (ElapsedMilliseconds < 500) + Thread.Sleep (500 - (int) ElapsedMilliseconds); + } + } + } +} |