/SparkleShare/SparkleEventLogController.cs
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://'>« Back</a> | "; 358 359 if (change_sets.Count > 1) 360 html += "Revisions for <b>“"; 361 else 362 html += "No revisions for <b>“"; 363 364 html += Path.GetFileName (file_path) + "”</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…</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 " ▾</a></small> "; 446 447 } else { 448 event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") + "</small> "; 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> "; 457 event_entry += FormatBreadCrumbs (change_set.Folder.FullPath, change.Path); 458 event_entry += "<br>"; 459 event_entry += "<small>" + change.Timestamp.ToString ("HH:mm") +"</small> "; 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}