summaryrefslogtreecommitdiff
path: root/SparkleShare/Common/EventLogController.cs
diff options
context:
space:
mode:
Diffstat (limited to 'SparkleShare/Common/EventLogController.cs')
-rw-r--r--SparkleShare/Common/EventLogController.cs646
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://'>&laquo; Back</a> &nbsp;|&nbsp; ";
+
+ if (change_sets.Count > 1)
+ html += "Revisions for <b>&ldquo;";
+ else
+ html += "No revisions for <b>&ldquo;";
+
+ html += Path.GetFileName (file_path) + "&rdquo;</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&hellip;</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") +
+ " &#x25BE;</a></small> &nbsp;";
+
+ } else {
+ event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") + "</small> &nbsp;";
+ }
+
+ 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> &nbsp;";
+ event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path);
+ event_entry += "<br>";
+ event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> &nbsp;";
+ 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);
+ }
+ }
+ }
+}