/SparkleShare/SparkleEventLogController.cs

http://github.com/hbons/SparkleShare · C# · 643 lines · 463 code · 163 blank · 17 comment · 94 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. using System;
  17. using System.Collections.Generic;
  18. using System.Diagnostics;
  19. using System.Globalization;
  20. using System.IO;
  21. using System.Text;
  22. using System.Text.RegularExpressions;
  23. using System.Threading;
  24. using SparkleLib;
  25. namespace SparkleShare {
  26. public class SparkleEventLogController {
  27. public event Action ShowWindowEvent = delegate { };
  28. public event Action HideWindowEvent = delegate { };
  29. public event Action ContentLoadingEvent = delegate { };
  30. public event UpdateContentEventEventHandler UpdateContentEvent = delegate { };
  31. public delegate void UpdateContentEventEventHandler (string html);
  32. public event UpdateChooserEventHandler UpdateChooserEvent = delegate { };
  33. public delegate void UpdateChooserEventHandler (string [] folders);
  34. public event UpdateChooserEnablementEventHandler UpdateChooserEnablementEvent = delegate { };
  35. public delegate void UpdateChooserEnablementEventHandler (bool enabled);
  36. public event UpdateSizeInfoEventHandler UpdateSizeInfoEvent = delegate { };
  37. public delegate void UpdateSizeInfoEventHandler (string size, string history_size);
  38. public event ShowSaveDialogEventHandler ShowSaveDialogEvent = delegate { };
  39. public delegate void ShowSaveDialogEventHandler (string file_name, string target_folder_path);
  40. private string selected_folder;
  41. private RevisionInfo restore_revision_info;
  42. private bool history_view_active;
  43. public bool WindowIsOpen { get; private set; }
  44. public string SelectedFolder {
  45. get {
  46. return this.selected_folder;
  47. }
  48. set {
  49. this.selected_folder = value;
  50. ContentLoadingEvent ();
  51. UpdateSizeInfoEvent ("…", "…");
  52. new Thread (() => {
  53. SparkleDelay delay = new SparkleDelay ();
  54. string html = HTML;
  55. delay.Stop ();
  56. if (!string.IsNullOrEmpty (html))
  57. UpdateContentEvent (html);
  58. UpdateSizeInfoEvent (Size, HistorySize);
  59. }).Start ();
  60. }
  61. }
  62. public string HTML {
  63. get {
  64. List<SparkleChangeSet> change_sets = GetLog (this.selected_folder);
  65. string html = GetHTMLLog (change_sets);
  66. return html;
  67. }
  68. }
  69. public string [] Folders {
  70. get {
  71. return Program.Controller.Folders.ToArray ();
  72. }
  73. }
  74. public string Size {
  75. get {
  76. double size = 0;
  77. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  78. if (this.selected_folder == null) {
  79. size += repo.Size;
  80. } else if (this.selected_folder.Equals (repo.Name)) {
  81. if (repo.Size == 0)
  82. return "???";
  83. else
  84. return repo.Size.ToSize ();
  85. }
  86. }
  87. if (size == 0)
  88. return "???";
  89. else
  90. return size.ToSize ();
  91. }
  92. }
  93. public string HistorySize {
  94. get {
  95. double size = 0;
  96. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  97. if (this.selected_folder == null) {
  98. size += repo.HistorySize;
  99. } else if (this.selected_folder.Equals (repo.Name)) {
  100. if (repo.HistorySize == 0)
  101. return "???";
  102. else
  103. return repo.HistorySize.ToSize ();
  104. }
  105. }
  106. if (size == 0)
  107. return "???";
  108. else
  109. return size.ToSize ();
  110. }
  111. }
  112. public SparkleEventLogController ()
  113. {
  114. Program.Controller.ShowEventLogWindowEvent += delegate {
  115. if (!WindowIsOpen) {
  116. ContentLoadingEvent ();
  117. UpdateSizeInfoEvent ("…", "…");
  118. if (this.selected_folder == null) {
  119. new Thread (() => {
  120. SparkleDelay delay = new SparkleDelay ();
  121. string html = HTML;
  122. delay.Stop ();
  123. UpdateChooserEvent (Folders);
  124. UpdateChooserEnablementEvent (true);
  125. if (!string.IsNullOrEmpty (html))
  126. UpdateContentEvent (html);
  127. UpdateSizeInfoEvent (Size, HistorySize);
  128. }).Start ();
  129. }
  130. }
  131. WindowIsOpen = true;
  132. ShowWindowEvent ();
  133. };
  134. Program.Controller.OnIdle += delegate {
  135. if (this.history_view_active)
  136. return;
  137. ContentLoadingEvent ();
  138. UpdateSizeInfoEvent ("…", "…");
  139. SparkleDelay delay = new SparkleDelay ();
  140. string html = HTML;
  141. delay.Stop ();
  142. if (!string.IsNullOrEmpty (html))
  143. UpdateContentEvent (html);
  144. UpdateSizeInfoEvent (Size, HistorySize);
  145. };
  146. Program.Controller.FolderListChanged += delegate {
  147. if (this.selected_folder != null && !Program.Controller.Folders.Contains (this.selected_folder))
  148. this.selected_folder = null;
  149. UpdateChooserEvent (Folders);
  150. UpdateSizeInfoEvent (Size, HistorySize);
  151. };
  152. }
  153. public void WindowClosed ()
  154. {
  155. WindowIsOpen = false;
  156. HideWindowEvent ();
  157. this.selected_folder = null;
  158. }
  159. public void LinkClicked (string url)
  160. {
  161. if (url.StartsWith ("about:") || string.IsNullOrEmpty (url))
  162. return;
  163. url = url.Replace ("%20", " ");
  164. if (url.StartsWith ("http")) {
  165. Program.Controller.OpenWebsite (url);
  166. } else if (url.StartsWith ("restore://") && this.restore_revision_info == null) {
  167. Regex regex = new Regex ("restore://(.+)/([a-f0-9]+)/(.+)/(.{3} [0-9]+ [0-9]+h[0-9]+)/(.+)");
  168. Match match = regex.Match (url);
  169. if (match.Success) {
  170. string author_name = match.Groups [3].Value;
  171. string timestamp = match.Groups [4].Value;
  172. this.restore_revision_info = new RevisionInfo () {
  173. Folder = new SparkleFolder (match.Groups [1].Value),
  174. Revision = match.Groups [2].Value,
  175. FilePath = Uri.UnescapeDataString (match.Groups [5].Value)
  176. };
  177. string file_name = Path.GetFileNameWithoutExtension (this.restore_revision_info.FilePath) +
  178. " (" + author_name + " " + timestamp + ")" + Path.GetExtension (this.restore_revision_info.FilePath);
  179. string target_folder_path = Path.Combine (this.restore_revision_info.Folder.FullPath,
  180. Path.GetDirectoryName (this.restore_revision_info.FilePath));
  181. ShowSaveDialogEvent (file_name, target_folder_path);
  182. }
  183. } else if (url.StartsWith ("back://")) {
  184. this.history_view_active = false;
  185. SelectedFolder = this.selected_folder; // TODO: Return to the same position on the page
  186. UpdateChooserEnablementEvent (true);
  187. } else if (url.StartsWith ("history://")) {
  188. this.history_view_active = true;
  189. ContentLoadingEvent ();
  190. UpdateSizeInfoEvent ("…", "…");
  191. UpdateChooserEnablementEvent (false);
  192. string folder = url.Replace ("history://", "").Split ("/".ToCharArray ()) [0];
  193. string file_path = url.Replace ("history://" + folder + "/", "");
  194. byte [] file_path_bytes = Encoding.Default.GetBytes (file_path);
  195. file_path = Encoding.UTF8.GetString (file_path_bytes);
  196. file_path = Uri.UnescapeDataString (file_path);
  197. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  198. if (!repo.Name.Equals (folder))
  199. continue;
  200. new Thread (() => {
  201. SparkleDelay delay = new SparkleDelay ();
  202. List<SparkleChangeSet> change_sets = repo.GetChangeSets (file_path);
  203. string html = GetHistoryHTMLLog (change_sets, file_path);
  204. delay.Stop ();
  205. if (!string.IsNullOrEmpty (html))
  206. UpdateContentEvent (html);
  207. }).Start ();
  208. break;
  209. }
  210. } else {
  211. Program.Controller.OpenFile (url);
  212. }
  213. }
  214. public void SaveDialogCompleted (string target_file_path)
  215. {
  216. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  217. if (repo.Name.Equals (this.restore_revision_info.Folder.Name)) {
  218. repo.RestoreFile (this.restore_revision_info.FilePath,
  219. this.restore_revision_info.Revision, target_file_path);
  220. break;
  221. }
  222. }
  223. this.restore_revision_info = null;
  224. Program.Controller.OpenFolder (Path.GetDirectoryName (target_file_path));
  225. }
  226. public void SaveDialogCancelled ()
  227. {
  228. this.restore_revision_info = null;
  229. }
  230. private List<SparkleChangeSet> GetLog ()
  231. {
  232. List<SparkleChangeSet> list = new List<SparkleChangeSet> ();
  233. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  234. List<SparkleChangeSet> change_sets = repo.ChangeSets;
  235. if (change_sets != null)
  236. list.AddRange (change_sets);
  237. else
  238. SparkleLogger.LogInfo ("Log", "Could not create log for " + repo.Name);
  239. }
  240. list.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp)));
  241. list.Reverse ();
  242. if (list.Count > 100)
  243. return list.GetRange (0, 100);
  244. else
  245. return list.GetRange (0, list.Count);
  246. }
  247. private List<SparkleChangeSet> GetLog (string name)
  248. {
  249. if (name == null)
  250. return GetLog ();
  251. foreach (SparkleRepoBase repo in Program.Controller.Repositories) {
  252. if (repo.Name.Equals (name)) {
  253. List<SparkleChangeSet> change_sets = repo.ChangeSets;
  254. if (change_sets != null)
  255. return change_sets;
  256. else
  257. break;
  258. }
  259. }
  260. return new List<SparkleChangeSet> ();
  261. }
  262. public string GetHistoryHTMLLog (List<SparkleChangeSet> change_sets, string file_path)
  263. {
  264. string html = "<div class='history-header'>" +
  265. "<a class='windows' href='back://'>&laquo; Back</a> &nbsp;|&nbsp; ";
  266. if (change_sets.Count > 1)
  267. html += "Revisions for <b>&ldquo;";
  268. else
  269. html += "No revisions for <b>&ldquo;";
  270. html += Path.GetFileName (file_path) + "&rdquo;</b>";
  271. html += "</div><div class='table-wrapper'><table>";
  272. if (change_sets.Count > 0)
  273. change_sets.RemoveAt (0);
  274. foreach (SparkleChangeSet change_set in change_sets) {
  275. html += "<tr>" +
  276. "<td class='avatar'><img src='" + GetAvatarFilePath (change_set.User) + "'></td>" +
  277. "<td class='name'>" + change_set.User.Name + "</td>" +
  278. "<td class='date'>" +
  279. change_set.Timestamp.ToString ("d MMM yyyy", CultureInfo.InvariantCulture) +
  280. "</td>" +
  281. "<td class='time'>" + change_set.Timestamp.ToString ("HH:mm") + "</td>" +
  282. "<td class='restore'>" +
  283. "<a href='restore://" + change_set.Folder.Name + "/" +
  284. change_set.Revision + "/" + change_set.User.Name + "/" +
  285. change_set.Timestamp.ToString ("MMM d H\\hmm", CultureInfo.InvariantCulture) + "/" +
  286. file_path + "'>Restore&hellip;</a>" +
  287. "</td>" +
  288. "</tr>";
  289. }
  290. html += "</table></div>";
  291. html = Program.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->", html);
  292. return html.Replace ("<!-- $midnight -->", "100000000");
  293. }
  294. public string GetHTMLLog (List<SparkleChangeSet> change_sets)
  295. {
  296. if (change_sets == null || change_sets.Count == 0)
  297. return Program.Controller.EventLogHTML.Replace ("<!-- $event-log-content -->",
  298. "<div class='day-entry'><div class='day-entry-header'>This project does not keep a history.</div></div>");
  299. List <ActivityDay> activity_days = new List <ActivityDay> ();
  300. change_sets.Sort ((x, y) => (x.Timestamp.CompareTo (y.Timestamp)));
  301. change_sets.Reverse ();
  302. foreach (SparkleChangeSet change_set in change_sets) {
  303. bool change_set_inserted = false;
  304. foreach (ActivityDay stored_activity_day in activity_days) {
  305. if (stored_activity_day.Date.Year == change_set.Timestamp.Year &&
  306. stored_activity_day.Date.Month == change_set.Timestamp.Month &&
  307. stored_activity_day.Date.Day == change_set.Timestamp.Day) {
  308. stored_activity_day.Add (change_set);
  309. change_set_inserted = true;
  310. break;
  311. }
  312. }
  313. if (!change_set_inserted) {
  314. ActivityDay activity_day = new ActivityDay (change_set.Timestamp);
  315. activity_day.Add (change_set);
  316. activity_days.Add (activity_day);
  317. }
  318. }
  319. string event_log_html = Program.Controller.EventLogHTML;
  320. string day_entry_html = Program.Controller.DayEntryHTML;
  321. string event_entry_html = Program.Controller.EventEntryHTML;
  322. string event_log = "";
  323. foreach (ActivityDay activity_day in activity_days) {
  324. string event_entries = "";
  325. foreach (SparkleChangeSet change_set in activity_day) {
  326. string event_entry = "<dl>";
  327. foreach (SparkleChange change in change_set.Changes) {
  328. if (change.Type != SparkleChangeType.Moved) {
  329. event_entry += "<dd class='" + change.Type.ToString ().ToLower () + "'>";
  330. if (!change.IsFolder) {
  331. event_entry += "<small><a href=\"history://" + change_set.Folder.Name + "/" +
  332. change.Path + "\" title=\"View revisions\">" + change.Timestamp.ToString ("HH:mm") +
  333. " &#x25BE;</a></small> &nbsp;";
  334. } else {
  335. event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") + "</small> &nbsp;";
  336. }
  337. event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path);
  338. event_entry += "</dd>";
  339. } else {
  340. event_entry += "<dd class='moved'>";
  341. event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> &nbsp;";
  342. event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path);
  343. event_entry += "<br>";
  344. event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> &nbsp;";
  345. event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.MovedToPath);
  346. event_entry += "</dd>";
  347. }
  348. }
  349. event_entry += "</dl>";
  350. string timestamp = change_set.Timestamp.ToString ("H:mm");
  351. if (!change_set.FirstTimestamp.Equals (new DateTime ()) &&
  352. !change_set.Timestamp.ToString ("H:mm").Equals (change_set.FirstTimestamp.ToString ("H:mm"))) {
  353. timestamp = change_set.FirstTimestamp.ToString ("H:mm") + " – " + timestamp;
  354. }
  355. // TODO: List commit messages if there are any
  356. event_entries += event_entry_html.Replace ("<!-- $event-entry-content -->", event_entry)
  357. .Replace ("<!-- $event-user-name -->", change_set.User.Name)
  358. .Replace ("<!-- $event-user-email -->", change_set.User.Email)
  359. .Replace ("<!-- $event-avatar-url -->", GetAvatarFilePath (change_set.User))
  360. .Replace ("<!-- $event-url -->", change_set.RemoteUrl.ToString ())
  361. .Replace ("<!-- $event-revision -->", change_set.Revision);
  362. if (this.selected_folder == null) {
  363. event_entries = event_entries.Replace ("<!-- $event-folder -->", " @ " + change_set.Folder.Name);
  364. event_entries = event_entries.Replace ("<!-- $event-folder-url -->", change_set.Folder.FullPath);
  365. }
  366. }
  367. string day_entry = "";
  368. DateTime today = DateTime.Now;
  369. DateTime yesterday = DateTime.Now.AddDays (-1);
  370. if (today.Day == activity_day.Date.Day &&
  371. today.Month == activity_day.Date.Month &&
  372. today.Year == activity_day.Date.Year) {
  373. day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
  374. "<span id='today' name='" +
  375. activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" + "Today" +
  376. "</span>");
  377. } else if (yesterday.Day == activity_day.Date.Day &&
  378. yesterday.Month == activity_day.Date.Month &&
  379. yesterday.Year == activity_day.Date.Year) {
  380. day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
  381. "<span id='yesterday' name='" + activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture) + "'>" +
  382. "Yesterday" +
  383. "</span>");
  384. } else {
  385. if (activity_day.Date.Year != DateTime.Now.Year) {
  386. day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
  387. activity_day.Date.ToString ("dddd, MMMM d, yyyy", CultureInfo.InvariantCulture));
  388. } else {
  389. day_entry = day_entry_html.Replace ("<!-- $day-entry-header -->",
  390. activity_day.Date.ToString ("dddd, MMMM d", CultureInfo.InvariantCulture));
  391. }
  392. }
  393. event_log += day_entry.Replace ("<!-- $day-entry-content -->", event_entries);
  394. }
  395. int midnight = (int) (DateTime.Today.AddDays (1) - new DateTime (1970, 1, 1)).TotalSeconds;
  396. string html = event_log_html.Replace ("<!-- $event-log-content -->", event_log);
  397. html = html.Replace ("<!-- $midnight -->", midnight.ToString ());
  398. return html;
  399. }
  400. private string FormatBreadCrumbs (string path_root, string path)
  401. {
  402. byte [] path_root_bytes = Encoding.Default.GetBytes (path_root);
  403. byte [] path_bytes = Encoding.Default.GetBytes (path);
  404. path_root = Encoding.UTF8.GetString (path_root_bytes);
  405. path = Encoding.UTF8.GetString (path_bytes);
  406. path_root = path_root.Replace ("/", Path.DirectorySeparatorChar.ToString ());
  407. path = path.Replace ("/", Path.DirectorySeparatorChar.ToString ());
  408. string new_path_root = path_root;
  409. string [] crumbs = path.Split (Path.DirectorySeparatorChar);
  410. string link = "";
  411. bool previous_was_folder = false;
  412. int i = 0;
  413. foreach (string crumb in crumbs) {
  414. if (string.IsNullOrEmpty (crumb))
  415. continue;
  416. string crumb_path = SafeCombine (new_path_root, crumb);
  417. if (Directory.Exists (crumb_path)) {
  418. link += "<a href='" + crumb_path + "'>" + crumb + Path.DirectorySeparatorChar + "</a>";
  419. previous_was_folder = true;
  420. } else if (File.Exists (crumb_path)) {
  421. link += "<a href='" + crumb_path + "'>" + crumb + "</a>";
  422. previous_was_folder = false;
  423. } else {
  424. if (i > 0 && !previous_was_folder)
  425. link += Path.DirectorySeparatorChar;
  426. link += crumb;
  427. previous_was_folder = false;
  428. }
  429. new_path_root = SafeCombine (new_path_root, crumb);
  430. i++;
  431. }
  432. return link;
  433. }
  434. private string SafeCombine (string path1, string path2)
  435. {
  436. string result = path1;
  437. if (!result.EndsWith (Path.DirectorySeparatorChar.ToString ()))
  438. result += Path.DirectorySeparatorChar;
  439. if (path2.StartsWith (Path.DirectorySeparatorChar.ToString ()))
  440. path2 = path2.Substring (1);
  441. return result + path2;
  442. }
  443. private string GetAvatarFilePath (SparkleUser user)
  444. {
  445. if (!Program.Controller.AvatarsEnabled)
  446. return "<!-- $pixmaps-path -->/user-icon-default.png";
  447. string fetched_avatar = SparkleAvatars.GetAvatar (user.Email, 48, Program.Controller.Config.FullPath);
  448. if (!string.IsNullOrEmpty (fetched_avatar))
  449. return "file://" + fetched_avatar.Replace ("\\", "/");
  450. else
  451. return "<!-- $pixmaps-path -->/user-icon-default.png";
  452. }
  453. // All change sets that happened on a day
  454. private class ActivityDay : List<SparkleChangeSet>
  455. {
  456. public DateTime Date;
  457. public ActivityDay (DateTime date_time)
  458. {
  459. Date = new DateTime (date_time.Year, date_time.Month, date_time.Day);
  460. }
  461. }
  462. private class RevisionInfo {
  463. public SparkleFolder Folder;
  464. public string FilePath;
  465. public string Revision;
  466. }
  467. private class SparkleDelay : Stopwatch {
  468. public SparkleDelay () : base ()
  469. {
  470. Start ();
  471. }
  472. new public void Stop ()
  473. {
  474. base.Stop ();
  475. if (ElapsedMilliseconds < 500)
  476. Thread.Sleep (500 - (int) ElapsedMilliseconds);
  477. }
  478. }
  479. }
  480. }