├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── GitTimelapseView.Core ├── Common │ ├── GitExecutableFinder.cs │ ├── GitHelpers.cs │ └── StringExtensions.cs ├── GitTimelapseView.Core.csproj └── Models │ ├── BlameBlock.cs │ ├── Commit.cs │ ├── FileChange.cs │ ├── FileHistory.cs │ └── FileRevision.cs ├── GitTimelapseView.Extensions ├── ActionState.cs ├── GitTimelapseView.Extensions.csproj ├── IAction.cs ├── IActionContext.cs ├── IAppInfo.cs ├── IPlugin.cs ├── IService.cs ├── ITelemetryProvider.cs ├── ITitleBarActionProvider.cs ├── IUserInfoProvider.cs ├── UserInfo.cs └── VisualFeedback.cs ├── GitTimelapseView.Tests ├── GitTimelapseView.Tests.csproj └── TimelapseHistoryTests.cs ├── GitTimelapseView.sln ├── GitTimelapseView ├── Actions │ ├── AboutAction.cs │ ├── ActionBase.cs │ ├── ChangeCurrentRevisionAction.cs │ ├── CopyToClipboardAction.cs │ ├── DiffFileChangeAction.cs │ ├── ExitApplicationAction.cs │ ├── OpenFileAction.cs │ ├── SelectCommitAction.cs │ └── ViewLogsAction.cs ├── Components │ ├── AvatarExt.razor │ ├── AvatarExt.razor.css │ ├── CommitInfo │ │ ├── CommitInfo.razor │ │ ├── CommitInfo.razor.cs │ │ ├── CommitInfo.razor.css │ │ ├── FileChanges.razor │ │ ├── FileChanges.razor.cs │ │ └── FileChanges.razor.css │ ├── Editor │ │ ├── TextEditor.razor │ │ ├── TextEditor.razor.cs │ │ ├── TextEditor.razor.css │ │ ├── TextEditorMargin.razor │ │ ├── TextEditorMargin.razor.cs │ │ └── TextEditorMargin.razor.css │ ├── HistorySlider.razor │ ├── HistorySlider.razor.css │ ├── PageProgress.razor │ ├── PageProgress.razor.css │ ├── Revisions │ │ ├── RevisionList.razor │ │ ├── RevisionList.razor.cs │ │ └── RevisionList.razor.css │ ├── ThemeProvider.razor │ └── Tooltips │ │ ├── RevisionTooltip.razor │ │ ├── RevisionTooltip.razor.cs │ │ ├── RevisionTooltip.razor.css │ │ ├── UserTooltip.razor │ │ ├── UserTooltip.razor.cs │ │ └── UserTooltip.razor.css ├── Data │ ├── CommitChangeReason.cs │ ├── CommitChangedEventArgs.cs │ ├── FileRevisionIndexChangeReason.cs │ └── FileRevisionIndexChangedEventArgs.cs ├── GitTimelapseView.csproj ├── GitTimelapseView.facade.xml ├── GitTimelapseView.nuspec ├── Helpers │ ├── ActionExtensions.cs │ ├── BindableBase.cs │ ├── DependencyInjectionExtensions.cs │ ├── FileExtensions.cs │ └── NotificationServiceExtensions.cs ├── Layouts │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── Pages │ ├── ErrorPage.razor │ ├── ErrorPage.razor.css │ ├── GettingStartedPage.razor │ ├── GettingStartedPage.razor.css │ ├── LoadingPage.razor │ ├── LoadingPage.razor.css │ ├── MainPage.razor │ └── MainPage.razor.css ├── Properties │ ├── Settings.Designer.cs │ └── Settings.settings ├── RazorApp.razor ├── Services │ ├── ActionService.cs │ ├── MessagingService.cs │ ├── PageProgressService.cs │ ├── PluginService.cs │ ├── ServiceBase.cs │ ├── TelemetryService.cs │ ├── ThemeInfo.cs │ ├── ThemingService.cs │ ├── TimelapseService.cs │ └── UserInfoService.cs ├── StartupOptions.cs ├── Wpf │ ├── App.xaml │ ├── App.xaml.cs │ ├── Helpers │ │ ├── ActionExtensions.cs │ │ └── FrameworkElementExtensions.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ └── Resources │ │ ├── appicon.ico │ │ └── appicon.png ├── _Imports.razor └── wwwroot │ ├── css │ ├── site.css │ ├── theme.dark.css │ └── theme.light.css │ ├── extern │ ├── mdi@6.5.95 │ │ ├── css │ │ │ └── materialdesignicons.min.css │ │ └── fonts │ │ │ └── materialdesignicons-webfont.woff2 │ └── threedots │ │ └── threedots.min.css │ ├── index.html │ └── scripts │ ├── GeneralScripts.js │ └── TextEditorScripts.js ├── LICENSE ├── README.md └── docs ├── img └── screenshot01.png └── screenshot01.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'main' 8 | tags: 9 | - '*' 10 | pull_request: 11 | branches: 12 | - '*' 13 | 14 | jobs: 15 | build_and_test: 16 | name: Build & Test 17 | runs-on: 'windows-latest' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Setup dotnet 22 | uses: actions/setup-dotnet@v2 23 | with: 24 | dotnet-version: 9.0.x 25 | - name: Run tests 26 | run: dotnet test --configuration Release 27 | release: 28 | name: Release 29 | needs: [ build_and_test ] 30 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 31 | strategy: 32 | matrix: 33 | kind: ['windows'] 34 | include: 35 | - kind: windows 36 | os: windows-latest 37 | target: win-x64 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v3 42 | - name: Setup dotnet 43 | uses: actions/setup-dotnet@v2 44 | with: 45 | dotnet-version: 9.0.x 46 | - name: Build 47 | shell: bash 48 | run: | 49 | tag=$(git describe --tags --abbrev=0) 50 | release_name="GitTimelapseView-$tag-${{ matrix.target }}" 51 | 52 | # Build everything 53 | dotnet publish GitTimelapseView/GitTimelapseView.csproj --framework net9.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" --self-contained 54 | 55 | # Pack files 56 | if [ "${{ matrix.target }}" == "win-x64" ]; then 57 | # Pack to zip for Windows 58 | 7z a -tzip "${release_name}.zip" "./${release_name}/*" 59 | else 60 | tar czvf "${release_name}.tar.gz" "$release_name" 61 | fi 62 | 63 | # Delete output directory 64 | rm -r "$release_name" 65 | - name: Publish 66 | uses: softprops/action-gh-release@v1 67 | with: 68 | files: "GitTimelapseView-*" 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #Ignore thumbnails created by Windows 3 | Thumbs.db 4 | #Ignore files built by Visual Studio 5 | *.obj 6 | *.exe 7 | *.pdb 8 | *.user 9 | *.aps 10 | *.pch 11 | *.vspscc 12 | *_i.c 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.cache 20 | *.ilk 21 | *.log 22 | [Bb]in 23 | [Dd]ebug*/ 24 | *.lib 25 | *.sbr 26 | obj/ 27 | [Rr]elease*/ 28 | _ReSharper*/ 29 | [Tt]est[Rr]esult* 30 | .vs/ 31 | #Nuget packages folder 32 | packages/ 33 | publish/ 34 | GitTimelapseView/Properties/launchSettings.json 35 | 36 | .idea/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | opensource@ubisoft.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/ubisoft/GitTimeLapseView/fork 4 | [pr]: https://github.com/ubisoft/GitTimeLapseView/compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Contributions to this project are released to the public under the [project's open source license](LICENSE). 10 | 11 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 12 | 13 | ## Submitting a pull request 14 | 15 | 1. [Fork][fork] and clone the repository 16 | 2. Make sure the tests pass on your machine 17 | 3. Create a new branch: `git checkout -b my-branch-name` 18 | 4. Make your change, add tests, and make sure the tests still pass 19 | 5. Push to your fork and [submit a pull request][pr] 20 | 6. Pat your self on the back and wait for your pull request to be reviewed and merged. 21 | 22 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 23 | 24 | - Follow the project style. 25 | - Write tests. 26 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 27 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 28 | 29 | ## Resources 30 | 31 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 32 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 33 | - [GitHub Help](https://help.github.com) 34 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Ubisoft 5 | Copyright © $([System.DateTime]::UtcNow.Year) $(Company) 6 | Tool to view and compare versions of a file 7 | GitTimelapseView 8 | 0.0.0 9 | 10 | 11 | 12 12 | net9.0-windows 13 | true 14 | strict 15 | True 16 | full 17 | NU1603 18 | $(SolutionDir)\bin\$(Configuration)\ 19 | false 20 | $(DefaultItemExcludes);publish/**/* 21 | enable 22 | false 23 | enable 24 | 25 | 26 | 27 | False 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers; buildtransitive 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Common/GitExecutableFinder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Win32; 5 | 6 | namespace GitTimelapseView.Common 7 | { 8 | public static class GitExecutableFinder 9 | { 10 | private static readonly string _configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "GitTimelapseView", "gitExe.config"); 11 | 12 | private static string? _gitExe; 13 | 14 | private static string? _gitBashRootDirectory; 15 | 16 | public static string? GitExe 17 | { 18 | get 19 | { 20 | if (string.IsNullOrEmpty(_gitExe)) 21 | { 22 | _gitExe = FindGitSystemToolset(); 23 | } 24 | 25 | return _gitExe; 26 | } 27 | } 28 | 29 | public static string? GitBashRootDirectory 30 | { 31 | get 32 | { 33 | if (string.IsNullOrEmpty(_gitBashRootDirectory)) 34 | { 35 | _gitBashRootDirectory = GitBashRootDirectoryImpl(); 36 | } 37 | 38 | return _gitBashRootDirectory; 39 | } 40 | } 41 | 42 | private static string? FindGitExeInConfigFile() 43 | { 44 | if (!File.Exists(_configFile)) 45 | return null; 46 | 47 | var configFileContent = File.ReadAllText(_configFile); 48 | 49 | configFileContent = AddQuotesIfContainsSpaces(configFileContent); 50 | 51 | return configFileContent.Contains("git.exe", StringComparison.OrdinalIgnoreCase) && File.Exists(configFileContent) 52 | ? configFileContent 53 | : null; 54 | } 55 | 56 | private static string AddQuotesIfContainsSpaces(string str) 57 | { 58 | if (!str.Contains(' ', StringComparison.Ordinal)) 59 | return str; 60 | 61 | if (!str.StartsWith('"')) 62 | { 63 | str = str.Insert(0, "\""); 64 | } 65 | 66 | if (!str.EndsWith('"')) 67 | { 68 | str += "\""; 69 | } 70 | 71 | return str; 72 | } 73 | 74 | private static string? FindGitExeInPathVariable() 75 | { 76 | var usualPaths = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine)?.Split(';') ?? []; 77 | 78 | foreach (var path in usualPaths) 79 | { 80 | var gitExecutablePath = Path.Combine(path, "git.exe"); 81 | 82 | if (File.Exists(gitExecutablePath)) 83 | return gitExecutablePath; 84 | } 85 | 86 | return null; 87 | } 88 | 89 | private static string? FindGitSystemToolset() 90 | { 91 | var gitPath = FindGitExeInConfigFile(); 92 | 93 | if (!string.IsNullOrEmpty(gitPath) && File.Exists(gitPath)) 94 | return gitPath; 95 | 96 | if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) 97 | { 98 | gitPath = FindGitExeInPathVariable(); 99 | } 100 | else 101 | { 102 | var defaultPath = Environment.ExpandEnvironmentVariables(@"%ProgramW6432%\git\cmd\git.exe"); 103 | gitPath = FindGitExeInPathVariable(); 104 | 105 | if (File.Exists(defaultPath)) 106 | return defaultPath; 107 | } 108 | 109 | if (gitPath != null && File.Exists(gitPath)) 110 | return gitPath; 111 | 112 | var gitBashRoot = GitBashRootDirectory; 113 | return gitBashRoot != null ? Path.Combine(gitBashRoot, "cmd", "git.exe") : null; 114 | } 115 | 116 | private static string? GitBashRootDirectoryImpl() 117 | { 118 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 119 | { 120 | foreach (var rootKey in new[] { RegistryHive.LocalMachine, RegistryHive.CurrentUser }) 121 | { 122 | foreach (var registryView in new[] { RegistryView.Registry64, RegistryView.Registry32 }) 123 | { 124 | var subKeyName = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; 125 | var uninstallPath = FindGitInstallPathInRegistry(RegistryKey.OpenBaseKey(rootKey, registryView), subKeyName); 126 | if (uninstallPath != null) 127 | return uninstallPath; 128 | } 129 | } 130 | } 131 | 132 | return null; 133 | } 134 | 135 | private static string? FindGitInstallPathInRegistry(RegistryKey root, string keyName) 136 | { 137 | using var uninstall = root.OpenSubKey(keyName, writable: false); 138 | if (uninstall == null) 139 | return null; 140 | 141 | foreach (var subKeyName in uninstall.GetSubKeyNames()) 142 | { 143 | using var softwareKey = uninstall.OpenSubKey(subKeyName); 144 | if (softwareKey == null) 145 | continue; 146 | 147 | if (softwareKey.GetValue("DisplayName") is not string displayName || !displayName.StartsWith("Git version ", StringComparison.OrdinalIgnoreCase)) 148 | continue; 149 | 150 | if (softwareKey.GetValue("InstallLocation") is string installLocation && File.Exists($@"{installLocation}\cmd\git.exe")) 151 | return installLocation; 152 | } 153 | 154 | return null; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Common/GitHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Diagnostics; 5 | using System.Text; 6 | using GitTimelapseView.Common; 7 | using LibGit2Sharp; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace GitTimelapseView.Core.Common 11 | { 12 | public static class GitHelpers 13 | { 14 | public static string RunGitCommand(string gitRootPath, string args, ILogger logger, string? onGitErrorMessage = null) 15 | { 16 | var processInfo = new ProcessStartInfo 17 | { 18 | UseShellExecute = false, 19 | RedirectStandardOutput = true, 20 | RedirectStandardError = true, 21 | StandardOutputEncoding = Encoding.UTF8, 22 | 23 | FileName = GitExecutableFinder.GitExe, 24 | Arguments = args, 25 | CreateNoWindow = true, 26 | WorkingDirectory = gitRootPath, 27 | }; 28 | 29 | logger.LogInformation($"{processInfo.FileName} {processInfo.Arguments}"); 30 | 31 | if (!File.Exists(processInfo.FileName)) 32 | { 33 | var message = "Could not find git executable. Please add git to your path variables or add path to git executable in plain text in a gitExe.config file in /Users/AppData/Local/GitTimelapseView/ directory"; 34 | throw new FileNotFoundException(message, "git.exe"); 35 | } 36 | 37 | var gitProcess = Process.Start(processInfo); 38 | 39 | if (gitProcess == null) 40 | throw new InvalidOperationException("Cannot start git.exe"); 41 | 42 | gitProcess.EnableRaisingEvents = true; 43 | gitProcess.Exited += (sender, _) => HandleGitCommandErrors(sender, logger, onGitErrorMessage); 44 | 45 | if (args.Contains("difftool", StringComparison.OrdinalIgnoreCase)) 46 | return string.Empty; 47 | 48 | return gitProcess.StandardOutput.ReadToEnd().Trim(); 49 | } 50 | 51 | public static string? GetRemotePlatform(string remoteUrl) 52 | { 53 | if (remoteUrl.Contains("github.com", StringComparison.OrdinalIgnoreCase)) 54 | { 55 | return "GitHub"; 56 | } 57 | else if (remoteUrl.Contains("gitlab", StringComparison.OrdinalIgnoreCase)) 58 | { 59 | return "GitLab"; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | internal static IReadOnlyList GetCommitFileLines(this Repository repository, string relativeFilePath, string sha) 66 | { 67 | var commit = repository.Lookup(sha); 68 | if (commit == null) 69 | return Array.Empty(); 70 | 71 | var treeEntry = commit[relativeFilePath]; 72 | var blob = (Blob)treeEntry.Target; 73 | var lines = new List(); 74 | 75 | using var contentStream = blob.GetContentStream(); 76 | using var reader = new StreamReader(contentStream, Encoding.UTF8); 77 | string? line; 78 | while ((line = reader.ReadLine()) != null) 79 | { 80 | lines.Add(line); 81 | } 82 | 83 | return lines; 84 | } 85 | 86 | internal static string? MakeRelativeFilePath(this Repository repository, string filePath) 87 | { 88 | var fullPath = new Uri(filePath, UriKind.Absolute); 89 | var relRoot = new Uri(repository.Info.WorkingDirectory, UriKind.Absolute); 90 | return relRoot.MakeRelativeUri(fullPath).ToString(); 91 | } 92 | 93 | internal static string? FindRemoteUrl(this Repository repository) 94 | { 95 | if (repository.Network.Remotes.Any()) 96 | { 97 | var remote = repository.Network.Remotes.First(); 98 | var url = remote.Url; 99 | if (url.EndsWith(".git", StringComparison.Ordinal)) 100 | { 101 | if (url.StartsWith("git", StringComparison.Ordinal)) 102 | { 103 | url = url.Replace(":", "/", StringComparison.Ordinal).Replace("git@", "https://", StringComparison.Ordinal); 104 | } 105 | 106 | return url.Replace(".git", string.Empty, StringComparison.Ordinal).TrimEnd('/'); 107 | } 108 | } 109 | 110 | return null; 111 | } 112 | 113 | internal static string? GetCommitUrl(string remoteUrl, string sha) 114 | { 115 | if (remoteUrl.Contains("github.com", StringComparison.OrdinalIgnoreCase)) 116 | { 117 | return $"{remoteUrl}/commit/{sha}"; 118 | } 119 | else if (remoteUrl.Contains("gitlab", StringComparison.OrdinalIgnoreCase)) 120 | { 121 | return $"{remoteUrl}/-/commit/{sha}"; 122 | } 123 | 124 | return null; 125 | } 126 | 127 | private static void HandleGitCommandErrors(object? sender, ILogger logger, string? onGitErrorMessage = null) 128 | { 129 | if (sender is Process gitProcess) 130 | { 131 | try 132 | { 133 | var standardError = gitProcess.StandardError.ReadToEnd(); 134 | var stdErrWithoutWarnings = standardError.Split('\n') 135 | .Where(line => !line.StartsWith("warning", StringComparison.OrdinalIgnoreCase)); 136 | 137 | var gitErrors = string.Join("\n", stdErrWithoutWarnings); 138 | 139 | if (!string.IsNullOrEmpty(gitErrors)) 140 | { 141 | var message = $"{onGitErrorMessage ?? string.Empty}" + 142 | $"{Environment.NewLine}{Environment.NewLine}" + 143 | $"Error executing 'git' {gitProcess.StartInfo.Arguments}:" + 144 | $"{Environment.NewLine}{Environment.NewLine}" + 145 | $"{standardError}"; 146 | logger.LogError(message); 147 | } 148 | } 149 | finally 150 | { 151 | gitProcess.Dispose(); 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Common/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Text; 5 | 6 | namespace GitTimelapseView.Common 7 | { 8 | internal static class StringExtensions 9 | { 10 | internal static string ReplaceFirst(this string text, string search, string replace, StringComparison comparisonType) 11 | { 12 | var index = text.IndexOf(search, comparisonType); 13 | if (index < 0) 14 | { 15 | return text; 16 | } 17 | 18 | return string.Concat(text.AsSpan(0, index), replace, text.AsSpan(index + search.Length)); 19 | } 20 | 21 | internal static string ConcatLines(this IReadOnlyList lines, int startLine, int lineCount) 22 | { 23 | var sb = new StringBuilder(); 24 | for (var i = startLine; i < startLine + lineCount; i++) 25 | { 26 | if (i >= 0 && i < lines.Count) 27 | { 28 | sb.AppendLine(lines[i]); 29 | } 30 | } 31 | 32 | return sb.ToString(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/GitTimelapseView.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | all 8 | runtime; build; native; contentfiles; analyzers; buildtransitive 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Models/BlameBlock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Common; 5 | using LibGit2Sharp; 6 | 7 | namespace GitTimelapseView.Core.Models 8 | { 9 | public class BlameBlock 10 | { 11 | public BlameBlock(BlameHunk block, FileRevision fileRevision, IReadOnlyList lines, string? remoteUrl) 12 | { 13 | InitialSignature = block.InitialSignature; 14 | FinalSignature = block.FinalSignature; 15 | InitialCommit = new Commit(block.InitialCommit, fileRevision.FileHistory, remoteUrl); 16 | FinalCommit = new Commit(block.FinalCommit, fileRevision.FileHistory, remoteUrl); 17 | StartLine = block.FinalStartLineNumber + 1; 18 | LineCount = block.LineCount; 19 | FileRevision = fileRevision.FileHistory.GetRevisionPerCommit(FinalCommit); 20 | Lines = lines; 21 | Text = lines.ConcatLines(block.FinalStartLineNumber, block.LineCount).Trim('\n'); 22 | } 23 | 24 | public Signature InitialSignature { get; } 25 | 26 | public Signature FinalSignature { get; } 27 | 28 | public Commit InitialCommit { get; } 29 | 30 | public Commit FinalCommit { get; } 31 | 32 | public int StartLine { get; } 33 | 34 | public int LineCount { get; } 35 | 36 | public FileRevision? FileRevision { get; } 37 | 38 | public IReadOnlyList Lines { get; } 39 | 40 | public string Text { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Models/Commit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Core.Common; 5 | using LibGit2Sharp; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Core.Models 9 | { 10 | public class Commit 11 | { 12 | private readonly List _fileChanges = []; 13 | 14 | public Commit(LibGit2Sharp.Commit commit, FileHistory fileHistory, string? remoteUrl) 15 | { 16 | Message = commit.Message; 17 | MessageShort = commit.MessageShort; 18 | Id = commit.Sha; 19 | ShortId = Id.Substring(0, 6); 20 | Author = commit.Author; 21 | Committer = commit.Committer; 22 | FileHistory = fileHistory; 23 | Parents = commit.Parents.Select(x => x.Sha).ToArray(); 24 | if (remoteUrl != null) 25 | { 26 | WebUrl = GitHelpers.GetCommitUrl(remoteUrl, commit.Sha); 27 | } 28 | } 29 | 30 | public string Message { get; init; } 31 | 32 | public string MessageShort { get; init; } 33 | 34 | public string Id { get; init; } 35 | 36 | public string ShortId { get; init; } 37 | 38 | public string[] Parents { get; init; } 39 | 40 | public Signature Author { get; } 41 | 42 | public Signature Committer { get; } 43 | 44 | public IReadOnlyList FileChanges => _fileChanges; 45 | 46 | public FileHistory FileHistory { get; } 47 | 48 | public string? ContainedInTag { get; private set; } 49 | 50 | public string? WebUrl { get; } 51 | 52 | public void UpdateInfo(ILogger logger) 53 | { 54 | if (FileChanges.Any()) 55 | return; 56 | 57 | using (var repository = new Repository(FileHistory.GitRootPath)) 58 | { 59 | var commit = repository.Lookup(Id); 60 | if (commit == null) 61 | return; 62 | 63 | var parents = commit?.Parents.ToArray() ?? []; 64 | var changes = repository.Diff.Compare(parents.FirstOrDefault()?.Tree, commit?.Tree); 65 | foreach (var change in changes) 66 | { 67 | _fileChanges.Add(new FileChange(this, change, repository.Info.WorkingDirectory)); 68 | } 69 | } 70 | 71 | if (ContainedInTag != null) 72 | return; 73 | 74 | var result = GitHelpers.RunGitCommand(FileHistory.GitRootPath, $"describe --contains {Id}", logger); 75 | if (result != null) 76 | { 77 | ContainedInTag = result.Split('~').First(); 78 | } 79 | } 80 | 81 | public Task UpdateInfoAsync(ILogger logger) 82 | { 83 | return Task.Run(() => UpdateInfo(logger)); 84 | } 85 | 86 | public bool IsEqualOrMergeOf(Commit other) 87 | { 88 | return Id.Equals(other.Id, StringComparison.OrdinalIgnoreCase) || IsMergeOf(other); 89 | } 90 | 91 | public bool IsMergeOf(Commit other) 92 | { 93 | return Parents.Length == 2 && Parents[1].Equals(other.Id, StringComparison.OrdinalIgnoreCase); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Models/FileChange.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using LibGit2Sharp; 5 | 6 | namespace GitTimelapseView.Core.Models 7 | { 8 | public sealed class FileChange 9 | { 10 | public FileChange(Commit commit, TreeEntryChanges change, string workingDirectory) 11 | { 12 | Commit = commit; 13 | ChangeKind = change.Status; 14 | Path = string.IsNullOrEmpty(change.Path) ? string.Empty : System.IO.Path.GetFullPath(System.IO.Path.Combine(workingDirectory, change.Path)); 15 | OldPath = string.IsNullOrEmpty(change.OldPath) ? string.Empty : System.IO.Path.GetFullPath(System.IO.Path.Combine(workingDirectory, change.OldPath)); 16 | Name = System.IO.Path.GetFileName(Path); 17 | } 18 | 19 | public ChangeKind ChangeKind { get; } 20 | 21 | public Commit Commit { get; } 22 | 23 | public string Name { get; } 24 | 25 | public string Path { get; } 26 | 27 | public string OldPath { get; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GitTimelapseView.Core/Models/FileRevision.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Core.Common; 5 | using LibGit2Sharp; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Core.Models 9 | { 10 | public class FileRevision 11 | { 12 | public FileRevision(int index, Commit commit, string commitIdFileName, FileHistory fileHistory) 13 | { 14 | Index = index; 15 | FileHistory = fileHistory; 16 | Commit = commit; 17 | CommitIdFileName = commitIdFileName; 18 | } 19 | 20 | public Commit Commit { get; } 21 | 22 | public int Index { get; } 23 | 24 | public string Label => $"{Index + 1}"; 25 | 26 | public FileHistory FileHistory { get; } 27 | 28 | public IList Blocks { get; } = new List(); 29 | 30 | /// 31 | /// Gets fileName at the time of the commit. 32 | /// 33 | private string CommitIdFileName { get; } 34 | 35 | public void LoadBlocks(ILogger logger) 36 | { 37 | if (Blocks.Any()) 38 | return; 39 | 40 | using var repository = new Repository(FileHistory.GitRootPath); 41 | var remoteUrl = repository.FindRemoteUrl(); 42 | 43 | var relativeFilePath = repository.MakeRelativeFilePath(CommitIdFileName); 44 | if (relativeFilePath == null) 45 | throw new Exception($"Unable to blame '{CommitIdFileName}'. Path is not located in the repository working directory."); 46 | 47 | var blocks = repository.Blame(relativeFilePath, new BlameOptions { StartingAt = Commit.Id }); 48 | var lines = repository.GetCommitFileLines(relativeFilePath, Commit.Id); 49 | 50 | foreach (var block in blocks) 51 | { 52 | Blocks.Add(new BlameBlock(block, this, lines, remoteUrl)); 53 | } 54 | } 55 | 56 | public async Task LoadBlocksAsync(ILogger logger) 57 | { 58 | await Task.Run(() => LoadBlocks(logger)).ConfigureAwait(false); 59 | } 60 | 61 | public override string ToString() 62 | { 63 | return $"#{Index} {Commit.ShortId} {Commit.MessageShort}"; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/ActionState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public enum ActionState 7 | { 8 | /// 9 | /// Unknown 10 | /// 11 | Unknown = 0, 12 | 13 | /// 14 | /// Running state 15 | /// 16 | Running = 1, 17 | 18 | /// 19 | /// Failed state 20 | /// 21 | Failed = 2, 22 | 23 | /// 24 | /// Success state 25 | /// 26 | Success = 3, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/GitTimelapseView.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | all 7 | runtime; build; native; contentfiles; analyzers; buildtransitive 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public interface IAction 7 | { 8 | /// 9 | /// Gets or sets the display name 10 | /// 11 | string DisplayName { get; set; } 12 | 13 | /// 14 | /// Gets or sets the action tooltip 15 | /// 16 | string? Tooltip { get; set; } 17 | 18 | /// 19 | /// Gets or sets the icon of the action. It can be a string representing an icon from MaterialDesignIcons, or an image url 20 | /// 21 | object? Icon { get; set; } 22 | 23 | /// 24 | /// Gets or sets the text describing an input gesture that will call the command tied to the specified item. 25 | /// 26 | string? InputGestureText { get; set; } 27 | 28 | /// 29 | /// Gets or sets the success notification text 30 | /// 31 | string? SuccessNotificationText { get; set; } 32 | 33 | /// 34 | /// Gets or sets the success notification text 35 | /// 36 | string? FailureNotificationText { get; set; } 37 | 38 | /// 39 | /// Gets or sets the children actions 40 | /// 41 | IAction[] Children { get; set; } 42 | 43 | /// 44 | /// Code that will be executed when action is started 45 | /// 46 | /// the action context. 47 | /// An execution task. 48 | Task ExecuteAsync(IActionContext context); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IActionContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace GitTimelapseView.Extensions 7 | { 8 | public interface IActionContext : ILogger 9 | { 10 | /// 11 | /// Gets current state 12 | /// 13 | ActionState State { get; } 14 | 15 | /// 16 | /// Gets or sets a value indicating whether to track this action or not 17 | /// 18 | bool IsTrackingEnabled { get; set; } 19 | 20 | /// 21 | /// Gets or sets an Id used for tracking. Defaults to action type name. 22 | /// 23 | string TrackingId { get; set; } 24 | 25 | /// 26 | /// Gets a dictionary with tracking properties that can be set 27 | /// 28 | IDictionary TrackingProperties { get; } 29 | 30 | /// 31 | /// Gets a dictionary Tracking metric that can be set 32 | /// 33 | IDictionary TrackingMetrics { get; } 34 | 35 | /// 36 | /// Gets or sets a value indicating the type of progress feedback 37 | /// 38 | VisualFeedback ProgressFeedback { get; set; } 39 | 40 | /// 41 | /// Gets or sets a message shown during progress 42 | /// 43 | string? ProgressMessage { get; set; } 44 | 45 | /// 46 | /// Gets or sets the last error message 47 | /// 48 | string? ErrorMessage { get; set; } 49 | 50 | /// 51 | /// Gets or sets the last exception 52 | /// 53 | Exception? Exception { get; set; } 54 | 55 | /// 56 | /// Gets or sets a value indicating the type of error feedback 57 | /// 58 | VisualFeedback ErrorFeedback { get; set; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IAppInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public interface IAppInfo 7 | { 8 | /// 9 | /// Gets the name of the application 10 | /// 11 | string ApplicationName { get; } 12 | 13 | /// 14 | /// Gets the version of the application 15 | /// 16 | string ApplicationVersion { get; } 17 | 18 | /// 19 | /// Gets the application data path 20 | /// 21 | string ApplicationDataPath { get; } 22 | 23 | /// 24 | /// Gets the log path 25 | /// 26 | string LogsPath { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace GitTimelapseView.Extensions 7 | { 8 | public interface IPlugin 9 | { 10 | /// 11 | /// Configure services 12 | /// 13 | /// a collection of services. 14 | void ConfigureServices(ServiceCollection serviceCollection); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace GitTimelapseView.Extensions 7 | { 8 | public interface IService 9 | { 10 | /// 11 | /// Gets a logger factory 12 | /// 13 | public ILoggerFactory LoggerFactory { get; } 14 | 15 | /// 16 | /// Gets a logger 17 | /// 18 | public ILogger Logger { get; } 19 | 20 | /// 21 | /// Initialization method 22 | /// 23 | /// an initializaiton task 24 | Task InitializeAsync(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/ITelemetryProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace GitTimelapseView.Extensions 7 | { 8 | public interface ITelemetryProvider 9 | { 10 | /// 11 | /// Initialization method 12 | /// 13 | public Task InitializeAsync(ILogger logger, IAppInfo appInfo); 14 | 15 | /// 16 | /// Add or update a global property that will be automatically added to all the events you send 17 | /// 18 | /// Do not add too many as it will increase the bandwidth used by the telemetry service 19 | void SetAdditionalInfo(string propertyName, object value); 20 | 21 | /// 22 | /// Track application page views 23 | /// 24 | void TrackPageView(string pageName, TimeSpan loadingTime = default, Uri? pageUrl = null); 25 | 26 | /// 27 | /// Track custom events (like action's calls) 28 | /// 29 | void TrackEvent(string name, IDictionary? properties = null, IDictionary? metrics = null); 30 | 31 | /// 32 | /// Track any kind of metrics (like cpu or memory usages) 33 | /// 34 | void TrackMetric(string name, double value, IDictionary? properties = null); 35 | 36 | /// 37 | /// Track any kind of exceptions 38 | /// 39 | void TrackException(Exception exception); 40 | 41 | /// 42 | /// Notify user activity (like an keyboard press or mouse click) 43 | /// 44 | void NotifyUserActivity(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/ITitleBarActionProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public interface ITitleBarActionProvider 7 | { 8 | /// 9 | /// Provide actions that will be added to title bar 10 | /// 11 | IEnumerable GetActions(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/IUserInfoProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public interface IUserInfoProvider 7 | { 8 | /// 9 | /// Get info about an user 10 | /// 11 | Task GetUserInfoAsync(string email); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/UserInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public class UserInfo 7 | { 8 | public UserInfo(string email) 9 | { 10 | Email = email; 11 | } 12 | 13 | /// 14 | /// Gets the email 15 | /// 16 | public string Email { get; } 17 | 18 | /// 19 | /// Gets the display name 20 | /// 21 | public string? DisplayName { get; init; } 22 | 23 | /// 24 | /// Gets the account name 25 | /// 26 | public string? AccountName { get; init; } 27 | 28 | /// 29 | /// Gets the job project 30 | /// 31 | public string? Project { get; init; } 32 | 33 | /// 34 | /// Gets the job title 35 | /// 36 | public string? Title { get; init; } 37 | 38 | /// 39 | /// Gets the location 40 | /// 41 | public string? Location { get; init; } 42 | 43 | /// 44 | /// Gets or sets the profile picture url 45 | /// 46 | public string? ProfilePictureUrl { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GitTimelapseView.Extensions/VisualFeedback.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Extensions 5 | { 6 | public enum VisualFeedback 7 | { 8 | /// 9 | /// No feedback 10 | /// 11 | None, 12 | 13 | /// 14 | /// A discrete progress bar on top of window 15 | /// 16 | ProgressBarTop, 17 | 18 | /// 19 | /// A message on top of window 20 | /// 21 | Message, 22 | 23 | /// 24 | /// A full screen feedback 25 | /// 26 | FullScreen, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GitTimelapseView.Tests/GitTimelapseView.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /GitTimelapseView.Tests/TimelapseHistoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Runtime.CompilerServices; 5 | using GitTimelapseView.Core.Models; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using NUnit.Framework; 8 | 9 | namespace GitTimelapseView.Tests 10 | { 11 | [TestFixture] 12 | public class TimelapseHistoryTests 13 | { 14 | [Test] 15 | public void TimelapseHistory_ForThisSourceFile_ContainsAtLeastTwoCommits() 16 | { 17 | var sourceDirectoryPath = Path.GetDirectoryName(GetThisSourceFilePath()); 18 | Assert.That(sourceDirectoryPath, Is.Not.Null); 19 | if (sourceDirectoryPath == null) 20 | { 21 | return; 22 | } 23 | 24 | var readmePath = Path.Combine(sourceDirectoryPath, "..", "README.md"); 25 | var history = new FileHistory(readmePath); 26 | history.Initialize(NullLogger.Instance); 27 | 28 | Assert.That(history, Is.Not.Null); 29 | Assert.That(history.Revisions.Count, Is.Not.Null); 30 | Assert.That(history.Revisions.Count, Is.GreaterThanOrEqualTo(1)); 31 | } 32 | 33 | private static string? GetThisSourceFilePath([CallerFilePath] string? srcFilePath = null) => srcFilePath; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GitTimelapseView.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31521.260 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0F0676E9-1533-4AD5-A9B1-6BEB52903EC1}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | Directory.Build.props = Directory.Build.props 10 | Directory.Build.targets = Directory.Build.targets 11 | readme.md = readme.md 12 | .github\workflows\ci.yml = .github\workflows\ci.yml 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitTimelapseView", "GitTimelapseView\GitTimelapseView.csproj", "{EDE8422E-984D-493F-B171-4DA13DDC8209}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitTimelapseView.Core", "GitTimelapseView.Core\GitTimelapseView.Core.csproj", "{CB812D9B-94B7-4835-A02E-8ADFC22477A9}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitTimelapseView.Extensions", "GitTimelapseView.Extensions\GitTimelapseView.Extensions.csproj", "{BE4ECDD6-8AE3-4E49-BD33-870F202A99AF}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitTimelapseView.Tests", "GitTimelapseView.Tests\GitTimelapseView.Tests.csproj", "{EB6D4AD1-4789-4732-99C4-FFF41E3E7EF1}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {EDE8422E-984D-493F-B171-4DA13DDC8209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {EDE8422E-984D-493F-B171-4DA13DDC8209}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {EDE8422E-984D-493F-B171-4DA13DDC8209}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {EDE8422E-984D-493F-B171-4DA13DDC8209}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {CB812D9B-94B7-4835-A02E-8ADFC22477A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {CB812D9B-94B7-4835-A02E-8ADFC22477A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {CB812D9B-94B7-4835-A02E-8ADFC22477A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CB812D9B-94B7-4835-A02E-8ADFC22477A9}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {BE4ECDD6-8AE3-4E49-BD33-870F202A99AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {BE4ECDD6-8AE3-4E49-BD33-870F202A99AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {BE4ECDD6-8AE3-4E49-BD33-870F202A99AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {BE4ECDD6-8AE3-4E49-BD33-870F202A99AF}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {EB6D4AD1-4789-4732-99C4-FFF41E3E7EF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {EB6D4AD1-4789-4732-99C4-FFF41E3E7EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {EB6D4AD1-4789-4732-99C4-FFF41E3E7EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {EB6D4AD1-4789-4732-99C4-FFF41E3E7EF1}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {83E26756-FC19-479A-9074-C74CC45932CF} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/AboutAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using GitTimelapseView.Services; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace GitTimelapseView.Actions 9 | { 10 | internal class AboutAction : ActionBase 11 | { 12 | public AboutAction() 13 | { 14 | DisplayName = "About"; 15 | Tooltip = $"About {nameof(GitTimelapseView)}"; 16 | Icon = "InfoCircleOutline"; 17 | } 18 | 19 | public override Task ExecuteAsync(IActionContext context) 20 | { 21 | var application = App.Current; 22 | if (application == null) 23 | return Task.CompletedTask; 24 | 25 | var messagingService = App.Current.ServiceProvider.GetService(); 26 | if (messagingService != null) 27 | { 28 | messagingService.ShowInformationDialog($"Version: {application.ApplicationVersion}"); 29 | } 30 | 31 | return Task.CompletedTask; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/ActionBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | 6 | namespace GitTimelapseView.Actions 7 | { 8 | public abstract class ActionBase : IAction 9 | { 10 | public string DisplayName { get; set; } = string.Empty; 11 | 12 | public string? Tooltip { get; set; } 13 | 14 | public object? Icon { get; set; } 15 | 16 | public virtual IAction[] Children { get; set; } = []; 17 | 18 | public string? InputGestureText { get; set; } = string.Empty; 19 | 20 | public string? SuccessNotificationText { get; set; } 21 | 22 | public string? FailureNotificationText { get; set; } 23 | 24 | public abstract Task ExecuteAsync(IActionContext context); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/ChangeCurrentRevisionAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Globalization; 5 | using GitTimelapseView.Extensions; 6 | using GitTimelapseView.Services; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace GitTimelapseView.Actions 11 | { 12 | public class ChangeCurrentRevisionAction : ActionBase 13 | { 14 | private readonly int _revisionIndex; 15 | 16 | public ChangeCurrentRevisionAction(int revisionIndex) 17 | { 18 | _revisionIndex = revisionIndex; 19 | DisplayName = "Change current Revision"; 20 | Tooltip = "View history of any file from a git repository"; 21 | Icon = "DateRange"; 22 | } 23 | 24 | public override async Task ExecuteAsync(IActionContext context) 25 | { 26 | context.LogInformation($"Selecting revision '{_revisionIndex}'"); 27 | context.TrackingProperties["RevisionIndex"] = _revisionIndex.ToString(CultureInfo.InvariantCulture); 28 | var timelapseService = App.Current.ServiceProvider.GetService(); 29 | if (timelapseService == null) 30 | { 31 | context.LogError($"Unable to get {nameof(TimelapseService)}"); 32 | return; 33 | } 34 | 35 | await timelapseService.SetCurrentFileRevisionIndexAsync(context, _revisionIndex).ConfigureAwait(false); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/CopyToClipboardAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using Humanizer; 6 | 7 | namespace GitTimelapseView.Actions 8 | { 9 | public class CopyToClipboardAction : ActionBase 10 | { 11 | private readonly string? _stringToCopy; 12 | private readonly string _stringToCopyDescription; 13 | 14 | public CopyToClipboardAction(string? stringToCopy, string stringToCopyDescription) 15 | { 16 | _stringToCopy = stringToCopy; 17 | _stringToCopyDescription = stringToCopyDescription; 18 | DisplayName = "Copy to clipboard"; 19 | SuccessNotificationText = $"{stringToCopyDescription.Transform(To.SentenceCase)} copied to clipboard"; 20 | FailureNotificationText = $"Failed to copy {stringToCopyDescription} to clipboard"; 21 | } 22 | 23 | public override Task ExecuteAsync(IActionContext context) 24 | { 25 | if (string.IsNullOrEmpty(_stringToCopy)) 26 | { 27 | throw new ArgumentNullException($"{_stringToCopyDescription.Transform(To.SentenceCase)} is null or empty"); 28 | } 29 | 30 | System.Windows.Clipboard.SetText(_stringToCopy); 31 | return Task.CompletedTask; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/DiffFileChangeAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.IO; 5 | using GitTimelapseView.Core.Common; 6 | using GitTimelapseView.Core.Models; 7 | using GitTimelapseView.Extensions; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace GitTimelapseView.Actions 11 | { 12 | internal class DiffFileChangeAction : ActionBase 13 | { 14 | private readonly FileChange _fileChange; 15 | private readonly string _commitId; 16 | 17 | public DiffFileChangeAction(FileChange fileChange, string commitId) 18 | { 19 | DisplayName = "Diff FileChange"; 20 | Tooltip = "Show Diff of a git file change"; 21 | Icon = "VectorDifference"; 22 | _fileChange = fileChange; 23 | _commitId = commitId; 24 | } 25 | 26 | public override Task ExecuteAsync(IActionContext context) 27 | { 28 | var oldPath = _fileChange.OldPath; 29 | var path = _fileChange.Path; 30 | 31 | context.TrackingProperties["Path"] = path; 32 | context.TrackingProperties["CommitId"] = _commitId; 33 | 34 | context.LogInformation($"Diffing '{path}' with commit '{_commitId}'"); 35 | 36 | var errorMessage = $"Could not launch difftool on {path} for commit {_commitId}"; 37 | var args = path.Equals(oldPath, StringComparison.OrdinalIgnoreCase) ? $"difftool -y {_commitId}^ {_commitId} {path}".Trim(' ') : $"difftool -y {_commitId}^ {_commitId} {oldPath} {path}".Trim(' '); 38 | GitHelpers.RunGitCommand(_fileChange.Commit.FileHistory.GitRootPath, args, context, errorMessage); 39 | 40 | return Task.CompletedTask; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/ExitApplicationAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | 6 | namespace GitTimelapseView.Actions 7 | { 8 | internal class ExitApplicationAction : ActionBase 9 | { 10 | public ExitApplicationAction() 11 | { 12 | DisplayName = "Exit"; 13 | Tooltip = "Exit the application"; 14 | Icon = null; 15 | InputGestureText = "Alt+F4"; 16 | } 17 | 18 | public override Task ExecuteAsync(IActionContext context) 19 | { 20 | context.IsTrackingEnabled = false; 21 | ((MainWindow)App.Current.MainWindow).ExitApplication(); 22 | return Task.CompletedTask; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/OpenFileAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using GitTimelapseView.Services; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Win32; 9 | 10 | namespace GitTimelapseView.Actions 11 | { 12 | public class OpenFileAction : ActionBase 13 | { 14 | private readonly string? _filePath; 15 | private readonly int? _lineNumber; 16 | private readonly int? _revisionIndex; 17 | private readonly string? _revisionSha; 18 | 19 | public OpenFileAction(string? filePath = null, int? lineNumber = null, int? revisionIndex = null, string? revisionSha = null) 20 | { 21 | DisplayName = "Open File..."; 22 | Tooltip = "View history of any file from a git repository"; 23 | Icon = "FolderOpen"; 24 | InputGestureText = "Ctrl+O"; 25 | 26 | _filePath = filePath; 27 | _lineNumber = lineNumber; 28 | _revisionIndex = revisionIndex; 29 | _revisionSha = revisionSha; 30 | } 31 | 32 | public override async Task ExecuteAsync(IActionContext context) 33 | { 34 | context.ProgressFeedback = VisualFeedback.None; 35 | var timelapseService = App.Current.ServiceProvider.GetService(); 36 | if (timelapseService == null) 37 | { 38 | context.LogError($"Unable to get {nameof(TimelapseService)}"); 39 | return; 40 | } 41 | 42 | var filePath = _filePath; 43 | if (string.IsNullOrEmpty(filePath)) 44 | { 45 | await App.Current.Dispatcher.InvokeAsync(() => 46 | { 47 | var openFileDialog = new OpenFileDialog(); 48 | if (openFileDialog.ShowDialog(App.Current.MainWindow) == true) 49 | { 50 | filePath = openFileDialog.FileName; 51 | } 52 | }); 53 | } 54 | 55 | if (!string.IsNullOrEmpty(filePath)) 56 | { 57 | context.LogInformation($"Opening file '{filePath}'"); 58 | context.ProgressMessage = $"Please wait while loading '{filePath}'..."; 59 | context.ProgressFeedback = VisualFeedback.FullScreen; 60 | context.TrackingProperties["FilePath"] = filePath; 61 | context.ErrorFeedback = VisualFeedback.FullScreen; 62 | await timelapseService.OpenFileAsync(context, filePath, _lineNumber, _revisionIndex, _revisionSha).ConfigureAwait(false); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/SelectCommitAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Core.Models; 5 | using GitTimelapseView.Extensions; 6 | using GitTimelapseView.Services; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace GitTimelapseView.Actions 11 | { 12 | internal class SelectCommitAction : ActionBase 13 | { 14 | private readonly Commit _commit; 15 | 16 | public SelectCommitAction(Commit commit) 17 | { 18 | _commit = commit; 19 | DisplayName = "Select Commit"; 20 | Tooltip = "Select a commit in the UI"; 21 | Icon = "SourceCommit"; 22 | } 23 | 24 | public override async Task ExecuteAsync(IActionContext context) 25 | { 26 | context.LogInformation($"Selecting commit '{_commit.Id}'"); 27 | context.IsTrackingEnabled = false; 28 | var timelapseService = App.Current.ServiceProvider.GetService(); 29 | if (timelapseService == null) 30 | { 31 | context.LogError($"Unable to get {nameof(TimelapseService)}"); 32 | return; 33 | } 34 | 35 | await timelapseService.SetCurrentCommitAsync(context, _commit).ConfigureAwait(false); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GitTimelapseView/Actions/ViewLogsAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | 6 | namespace GitTimelapseView.Actions 7 | { 8 | internal class ViewLogsAction : ActionBase 9 | { 10 | public ViewLogsAction() 11 | { 12 | DisplayName = "View Error Logs"; 13 | Tooltip = "View Error Logs"; 14 | Icon = "Text"; 15 | } 16 | 17 | public override Task ExecuteAsync(IActionContext context) 18 | { 19 | Process.Start("explorer.exe", App.Current.LogsPath); 20 | return Task.CompletedTask; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/AvatarExt.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | @if(_userInfo != null) 4 | { 5 | 6 | @if (ShowDisplayName) 7 | { 8 | 9 | @_userInfo.DisplayName 10 | 11 | } 12 | @if (_userInfo != null) 13 | { 14 | 15 | } 16 | 17 | } 18 | 19 | @code { 20 | [Inject] 21 | UserInfoService UserInfoService { get; set; } = default!; 22 | 23 | [Parameter] 24 | public string Email { get; set; } = default!; 25 | 26 | [Parameter] 27 | public Placement PopupPlacement { get; set; } = Placement.Top; 28 | 29 | [Parameter] 30 | public bool ShowDisplayName { get; set; } = false; 31 | 32 | private UserInfo? _userInfo; 33 | 34 | protected override async Task OnParametersSetAsync() 35 | { 36 | _userInfo = await UserInfoService.GetUserInfoAsync(Email).ConfigureAwait(false); 37 | await base.OnParametersSetAsync(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/AvatarExt.razor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/Components/AvatarExt.razor.css -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/CommitInfo.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | @using System.Diagnostics 3 | @using GitTimelapseView.Core.Common 4 | 5 | @if (_commit == null) 6 | { 7 | 8 | 9 | } 10 | else 11 | { 12 | 13 | 14 |
15 | 16 | Commit #@_commit.ShortId 17 | 18 | 19 | 22 | 23 | by 24 | 25 | 26 | 27 | on @_authoredString 28 | @if(_commit.WebUrl != null && GitHelpers.GetRemotePlatform(_commit.WebUrl) is string platform) 29 | { 30 |  @platform 31 | } 32 |
33 |
34 | 35 | @if(!string.IsNullOrEmpty(_commit.ContainedInTag)) 36 | { 37 | 38 | 39 | 40 | @_commit.ContainedInTag 41 | 42 | 43 | } 44 | 45 | 46 | 47 | 48 |
49 | @_commit.Message 50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 |
58 | } 59 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/CommitInfo.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using AntDesign; 3 | using GitTimelapseView.Actions; 4 | using GitTimelapseView.Core.Models; 5 | using GitTimelapseView.Helpers; 6 | using GitTimelapseView.Services; 7 | using Microsoft.AspNetCore.Components; 8 | 9 | namespace GitTimelapseView; 10 | 11 | public partial class CommitInfo 12 | { 13 | [Inject] private TimelapseService TimelapseService { get; set; } = default!; 14 | 15 | [Inject] private MessageService MessageService { get; set; } = default!; 16 | private Commit? _commit; 17 | private string _authoredString => _commit?.Author.When.ToString("dd/MM/yyyy @ hh:mm", CultureInfo.InvariantCulture) ?? string.Empty; 18 | private const string CopyToClipboardTooltip = "Copy complete id to clipboard"; 19 | 20 | protected override async Task OnInitializedAsync() 21 | { 22 | TimelapseService.CurrentCommitChanged += async (_, _) => await UpdateAsync().ConfigureAwait(false); 23 | await UpdateAsync().ConfigureAwait(false); 24 | await base.OnInitializedAsync().ConfigureAwait(false); 25 | } 26 | 27 | private async Task UpdateAsync() 28 | { 29 | if (TimelapseService.FileHistory != null && TimelapseService.CurrentCommit != null) 30 | { 31 | _commit = TimelapseService.CurrentCommit; 32 | } 33 | 34 | await InvokeAsync(StateHasChanged); 35 | } 36 | 37 | private async Task OnButtonClicked() 38 | { 39 | if (TimelapseService.CurrentCommit != null) 40 | { 41 | await new CopyToClipboardAction(TimelapseService.CurrentCommit.Id, "commit id").ExecuteAsync().ConfigureAwait(false); 42 | } 43 | } 44 | 45 | private void OnWebUrlClicked() 46 | { 47 | if (_commit?.WebUrl != null) 48 | { 49 | Process.Start(new ProcessStartInfo { FileName = _commit?.WebUrl, UseShellExecute = true, }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/CommitInfo.razor.css: -------------------------------------------------------------------------------- 1 | .card-title { 2 | font-weight: bolder; 3 | font-size: 12px; 4 | } 5 | 6 | .open-remote-url { 7 | opacity: 0.4; 8 | margin-left: 10px; 9 | font-size: 12px; 10 | color: var(--gtlv-foreground); 11 | text-decoration: none; 12 | } 13 | 14 | .tag-label { 15 | opacity: 0.4; 16 | margin-left: 20px; 17 | font-size: 12px; 18 | } 19 | 20 | .commit-message { 21 | margin: 10px; 22 | overflow: scroll; 23 | max-height: 220px; 24 | max-width: 750px; 25 | overflow-y: scroll; 26 | overflow-x: scroll; 27 | white-space: pre-wrap; 28 | } 29 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/FileChanges.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | @using LibGit2Sharp 3 | 4 | 12 | 13 | @if(@context.ChangeKind == ChangeKind.Modified) 14 | { 15 | 16 | }else if(@context.ChangeKind == ChangeKind.Added) 17 | { 18 | 19 | }else if(@context.ChangeKind == ChangeKind.Deleted) 20 | { 21 | 22 | }else if(@context.ChangeKind == ChangeKind.Renamed) 23 | { 24 | 25 | }else if(@context.ChangeKind == ChangeKind.Conflicted) 26 | { 27 | 28 | } 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 47 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/FileChanges.razor.cs: -------------------------------------------------------------------------------- 1 | using GitTimelapseView.Actions; 2 | using GitTimelapseView.Core.Models; 3 | using GitTimelapseView.Helpers; 4 | using LibGit2Sharp; 5 | using Microsoft.AspNetCore.Components; 6 | 7 | namespace GitTimelapseView; 8 | 9 | public partial class FileChanges 10 | { 11 | private const string DiffTooltip = "View diff using default diff tool"; 12 | private FileChangeTableRow[] Rows => Changes.Select(x => new FileChangeTableRow(x)).ToArray(); 13 | private const int PageSize = 100; 14 | private bool _hidePagination => Changes.Count < PageSize; 15 | private string _scrollY => _hidePagination ? "220px" : "160px"; 16 | 17 | [Parameter] public string CommitId { get; set; } = default!; 18 | 19 | [Parameter] public IReadOnlyList Changes { get; set; } = default!; 20 | 21 | public void OnRowClicked(FileChangeTableRow fileChangeRow) 22 | { 23 | _ = new DiffFileChangeAction(fileChangeRow.FileChange, CommitId).ExecuteAsync().ConfigureAwait(false); 24 | } 25 | 26 | public void OnPathClicked(FileChangeTableRow fileChangeRow) 27 | { 28 | _ = new OpenFileAction(fileChangeRow.Path, revisionSha: fileChangeRow.FileChange.Commit.Id).ExecuteAsync().ConfigureAwait(false); 29 | } 30 | 31 | public class FileChangeTableRow 32 | { 33 | public FileChangeTableRow(FileChange fileChange) 34 | { 35 | ChangeKind = fileChange.ChangeKind; 36 | Path = fileChange.Path; 37 | FileChange = fileChange; 38 | } 39 | 40 | public ChangeKind ChangeKind { get; set; } 41 | 42 | public string Path { get; set; } 43 | 44 | public FileChange FileChange { get; } 45 | 46 | public string RowClass => "filechange"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/CommitInfo/FileChanges.razor.css: -------------------------------------------------------------------------------- 1 | .change-icon { 2 | font-size: 18px; 3 | line-height: 18px; 4 | } 5 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditor.razor: -------------------------------------------------------------------------------- 1 | @using BlazorMonaco.Editor 2 | @namespace GitTimelapseView 3 | 4 |
5 |
6 | 7 |
8 |
9 | 13 |
14 |
15 | 16 | 43 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditor.razor.cs: -------------------------------------------------------------------------------- 1 | using BlazorMonaco; 2 | using BlazorMonaco.Editor; 3 | using GitTimelapseView.Actions; 4 | using GitTimelapseView.Data; 5 | using GitTimelapseView.Helpers; 6 | using GitTimelapseView.Services; 7 | using Microsoft.AspNetCore.Components; 8 | using Microsoft.JSInterop; 9 | 10 | namespace GitTimelapseView; 11 | 12 | public partial class TextEditor 13 | { 14 | private CodeEditor? _editor; 15 | private TextModel? _model; 16 | private string _previousValue = string.Empty; 17 | private List _currentDecorations = []; 18 | private bool _didInit; 19 | 20 | [Inject] private TimelapseService TimelapseService { get; set; } = default!; 21 | 22 | [Inject] private IJSRuntime JsRuntime { get; set; } = default!; 23 | 24 | [Inject] private ThemingService ThemingService { get; set; } = default!; 25 | 26 | protected override Task OnInitializedAsync() 27 | { 28 | TimelapseService.CurrentFileRevisionIndexChanged += async (_, args) => 29 | { 30 | if (args.Reason == FileRevisionIndexChangeReason.Explicit) 31 | { 32 | await UpdateModelAsync(); 33 | } 34 | }; 35 | TimelapseService.CurrentCommitChanged += async (_, args) => 36 | { 37 | if (args.Reason == CommitChangeReason.Explicit) 38 | { 39 | await UpdateDecorationsAsync(); 40 | } 41 | }; 42 | return base.OnInitializedAsync(); 43 | } 44 | 45 | private async Task UpdateModelAsync() 46 | { 47 | if (_editor == null || !_didInit || TimelapseService.CurrentFileRevision == null) 48 | return; 49 | 50 | var newValue = string.Join("\n", TimelapseService.CurrentFileRevision.Blocks.Select(x => x.Text)); 51 | if (string.Equals(newValue, _previousValue, StringComparison.Ordinal)) 52 | return; 53 | 54 | if (_model == null) 55 | { 56 | try 57 | { 58 | var language = "plaintext"; 59 | if (TimelapseService.FilePath is { } filePath) 60 | { 61 | var extension = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); 62 | if (FileExtensions.ExtensionsToLanguage.TryGetValue(extension, out var l)) 63 | { 64 | language = l; 65 | } 66 | } 67 | _model = await Global.CreateModel(JsRuntime, newValue, language: language); 68 | await _editor.SetModel(_model); 69 | if (TimelapseService.InitialLineNumber != null) 70 | { 71 | await _editor.RevealLineInCenter(TimelapseService.InitialLineNumber.Value, ScrollType.Smooth).ConfigureAwait(false); 72 | await SelectLineNumberAsync(TimelapseService.InitialLineNumber.Value).ConfigureAwait(false); 73 | } 74 | } 75 | catch (Exception) 76 | { 77 | _model = await _editor.GetModel().ConfigureAwait(false); 78 | } 79 | } 80 | 81 | await _model.SetValue(newValue).ConfigureAwait(false); 82 | _previousValue = newValue; 83 | 84 | await UpdateDecorationsAsync(_model).ConfigureAwait(false); 85 | } 86 | 87 | private StandaloneEditorConstructionOptions EditorConstructionOptions(Editor editor) 88 | { 89 | return new StandaloneEditorConstructionOptions 90 | { 91 | AutomaticLayout = true, 92 | GlyphMargin = true, 93 | ReadOnly = true, 94 | ScrollBeyondLastLine = false, 95 | Theme = ThemingService.Theme.MonacoTheme, 96 | }; 97 | } 98 | 99 | private async Task EditorOnDidInit() 100 | { 101 | _didInit = true; 102 | await UpdateModelAsync(); 103 | } 104 | 105 | private async Task EditorOnDidScroll(ScrollEvent scrollEvent) 106 | { 107 | await JsRuntime.InvokeVoidAsync($"setScroll", ".editor-custommargin", scrollEvent.ScrollTop, scrollEvent.ScrollHeight); 108 | } 109 | 110 | private Task EditorOnMouseDown(EditorMouseEvent mouseEvent) 111 | { 112 | if (mouseEvent.Target.Type == MouseTargetType.SCROLLBAR || mouseEvent.Target?.Position == null) 113 | return Task.CompletedTask; 114 | 115 | return SelectLineNumberAsync(mouseEvent.Target.Position.LineNumber); 116 | } 117 | 118 | private async Task SelectLineNumberAsync(int lineNumber) 119 | { 120 | var block = TimelapseService.CurrentFileRevision?.Blocks.FirstOrDefault(x => lineNumber >= x.StartLine && lineNumber < x.StartLine + x.LineCount); 121 | if (block != null) 122 | { 123 | await new SelectCommitAction(block.InitialCommit).ExecuteAsync(); 124 | } 125 | } 126 | 127 | private async Task UpdateDecorationsAsync(TextModel? model = null) 128 | { 129 | if (_editor == null || !_didInit || TimelapseService.CurrentFileRevision == null) 130 | return; 131 | 132 | model ??= await _editor.GetModel().ConfigureAwait(false); 133 | 134 | var index = 0; 135 | var newDecorations = new List(); 136 | foreach (var block in TimelapseService.CurrentFileRevision.Blocks) 137 | { 138 | var isPair = index % 2 == 0; 139 | var isCommitSelected = string.Equals(block.InitialCommit.Id, TimelapseService.CurrentCommit?.Id, StringComparison.Ordinal) || 140 | string.Equals(block.FinalCommit.Id, TimelapseService.CurrentCommit?.Id, StringComparison.Ordinal); 141 | newDecorations.Add(new ModelDeltaDecoration 142 | { 143 | Range = new BlazorMonaco.Range(block.StartLine, 0, block.StartLine + block.LineCount - 1, 1), 144 | Options = new ModelDecorationOptions { IsWholeLine = true, ClassName = isCommitSelected ? "editorHighlighted" : null, GlyphMarginClassName = isPair ? "editorBlockMargin" : "editorBlockMarginAlternate" }, 145 | }); 146 | index++; 147 | } 148 | 149 | _currentDecorations = await model.DeltaDecorations(_currentDecorations, newDecorations, null).ConfigureAwait(false); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditor.razor.css: -------------------------------------------------------------------------------- 1 | .text-editor-grid { 2 | display: grid; 3 | grid-template-rows: 1fr; 4 | grid-template-columns: auto 1fr; 5 | height: 100%; 6 | width: 100%; 7 | overflow: scroll; 8 | border: 1px solid; 9 | border-color: var(--gtlv-card-border); 10 | } 11 | 12 | .margin-box { 13 | grid-row: 1; 14 | grid-column: 1; 15 | overflow: hidden; 16 | } 17 | 18 | .editor-box { 19 | grid-row: 1; 20 | grid-column: 2; 21 | overflow: hidden; 22 | } 23 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditorMargin.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 |
4 | @if (TimelapseService.CurrentFileRevision != null) 5 | { 6 | @foreach(var block in @TimelapseService.CurrentFileRevision.Blocks) 7 | { 8 |
9 |
10 | @if(@block.FileRevision != null) 11 | { 12 | 13 | 14 | @(block.FileRevision.Label) 15 | 16 | 17 | 18 | 19 | } 20 | 21 | 22 |
23 |
24 | } 25 | } 26 | 27 |
28 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditorMargin.razor.cs: -------------------------------------------------------------------------------- 1 | using GitTimelapseView.Actions; 2 | using GitTimelapseView.Core.Models; 3 | using GitTimelapseView.Helpers; 4 | using GitTimelapseView.Services; 5 | using Microsoft.AspNetCore.Components; 6 | 7 | namespace GitTimelapseView; 8 | 9 | public partial class TextEditorMargin 10 | { 11 | private const int RowHeight = 19; 12 | 13 | [Parameter] public string CssClass { get; set; } = ""; 14 | 15 | [Inject] private TimelapseService TimelapseService { get; set; } = default!; 16 | 17 | protected override Task OnInitializedAsync() 18 | { 19 | TimelapseService.CurrentFileRevisionIndexChanged += async (_, _) => await InvokeAsync(StateHasChanged); 20 | return base.OnInitializedAsync(); 21 | } 22 | 23 | private async Task OnBlockClicked(BlameBlock block) 24 | { 25 | await new SelectCommitAction(block.InitialCommit).ExecuteAsync(); 26 | } 27 | 28 | private string getBlockStyle(BlameBlock block) => $"height:{RowHeight * block.LineCount}px;max-height:{RowHeight * block.LineCount}px;margin:0px;"; 29 | } 30 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Editor/TextEditorMargin.razor.css: -------------------------------------------------------------------------------- 1 | .editor-margin { 2 | height: 100%; 3 | margin: 0px; 4 | overflow: hidden; 5 | background-color: var(--gtlv-card-background); 6 | user-select: none; 7 | } 8 | 9 | .revision-row { 10 | user-select: none; 11 | } 12 | 13 | .revision-span { 14 | width: 18px; 15 | display: inline-block; 16 | text-align: right; 17 | font-size: 10px; 18 | margin-top: 4px; 19 | margin-left: 1px; 20 | opacity: 0.5; 21 | user-select: none; 22 | } 23 | 24 | .avatar-span { 25 | display: inline-block; 26 | margin-top: -4px; 27 | margin-left: 2px; 28 | user-select: none; 29 | } 30 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/HistorySlider.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | @using System.Windows 3 | 4 |
5 | @if (TimelapseService.FileHistory != null) 6 | { 7 | 8 |
9 | @for (int i = MinValue ; i <= MaxValue ; i++) 10 | { 11 | var revision = TimelapseService.FileHistory.Revisions[i]; 12 | var label = GetRevisionLabel(revision); 13 | if (!string.IsNullOrEmpty(label)) 14 | { 15 | 16 |

@label

17 |
18 | } 19 | } 20 |
21 | } 22 |
23 | 24 | @code 25 | { 26 | private const double RequiredSizePerRevision = 24.0; 27 | private const int MaximumRevision = 100; 28 | private SamplingCache _samplingCache; 29 | 30 | [Inject] 31 | TimelapseService TimelapseService { get; set; } = default!; 32 | 33 | [Inject] 34 | MessagingService MessagingService { get; set; } = default!; 35 | 36 | private double PixelsPerRevision => RevisionCount > 0 ? Application.Current.MainWindow.ActualWidth / RevisionCount : 0; 37 | 38 | private int CurrentValue => TimelapseService.CurrentFileRevision?.Index ?? 0; 39 | 40 | private int MinValue => Math.Max(0, MaxValue - MaximumRevision); 41 | 42 | private int MaxValue => TimelapseService.FileHistory != null ? TimelapseService.FileHistory.Revisions.Count - 1 : 0; 43 | 44 | private int RevisionCount => (int)(MaxValue - MinValue); 45 | 46 | private SliderMark[] Marks 47 | { 48 | get 49 | { 50 | var marks = new List(); 51 | if (TimelapseService.FileHistory != null) 52 | { 53 | foreach(var revision in TimelapseService.FileHistory.Revisions) 54 | { 55 | var label = GetRevisionLabel(revision); 56 | if (!string.IsNullOrEmpty(label)) 57 | { 58 | marks.Add(new SliderMark(revision.Index, GetMarkContent(revision), "")); 59 | } 60 | } 61 | } 62 | return marks.ToArray(); 63 | } 64 | } 65 | 66 | private RenderFragment GetMarkContent(FileRevision revision) => 67 | @ 68 | @revision.Label 69 | 70 | ; 71 | 72 | public string GetRevisionLabel(FileRevision fileRevision) 73 | { 74 | var label = fileRevision.Label; 75 | var sampling = ComputeSampling(); 76 | if (sampling > 1) 77 | { 78 | label = fileRevision.Index % sampling == 0 || fileRevision == TimelapseService.CurrentFileRevision ? label: string.Empty; 79 | } 80 | 81 | return label; 82 | } 83 | 84 | protected override Task OnInitializedAsync() 85 | { 86 | TimelapseService.CurrentFileRevisionIndexChanging += async (_, _) => await RefreshAsync(); 87 | Application.Current.MainWindow.SizeChanged += async (_, _) => await RefreshAsync(); 88 | return base.OnInitializedAsync(); 89 | } 90 | 91 | 92 | private async Task OnRevisionChange(ChangeEventArgs eventArgs) 93 | { 94 | if (eventArgs.Value != null && int.TryParse(eventArgs.Value.ToString(), out var revisionIndex)) 95 | { 96 | await new ChangeCurrentRevisionAction(revisionIndex).ExecuteAsync().ConfigureAwait(false); 97 | } 98 | } 99 | 100 | 101 | private int ComputeSampling() 102 | { 103 | if (_samplingCache.PixelsPerRevision == PixelsPerRevision && _samplingCache.RevisionCount == RevisionCount) 104 | { 105 | return _samplingCache.Sampling; 106 | } 107 | var sampling = 1; 108 | if (PixelsPerRevision < RequiredSizePerRevision) 109 | { 110 | var pixelsPerRevision = PixelsPerRevision; 111 | while (pixelsPerRevision <= RequiredSizePerRevision && sampling < 50) 112 | { 113 | sampling++; 114 | pixelsPerRevision = PixelsPerRevision * sampling; 115 | } 116 | } 117 | 118 | _samplingCache.PixelsPerRevision = PixelsPerRevision; 119 | _samplingCache.RevisionCount = RevisionCount; 120 | _samplingCache.Sampling = sampling; 121 | return sampling; 122 | } 123 | 124 | public Task RefreshAsync() 125 | { 126 | return InvokeAsync(() => StateHasChanged()); 127 | } 128 | 129 | struct SamplingCache 130 | { 131 | public double PixelsPerRevision; 132 | public int RevisionCount; 133 | public int Sampling; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/HistorySlider.razor.css: -------------------------------------------------------------------------------- 1 | 2 | input { 3 | width: 100%; 4 | } 5 | 6 | .history-slider { 7 | margin-top: 1rem; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .sliderticks { 12 | display: flex; 13 | justify-content: space-between; 14 | padding: 0 5px; 15 | user-select: none; 16 | } 17 | 18 | .sliderticks p { 19 | position: relative; 20 | display: flex; 21 | justify-content: center; 22 | text-align: center; 23 | width: 1px; 24 | background-color: var(--gtlv-foreground); 25 | opacity: 0.6; 26 | height: 5px; 27 | line-height: 30px; 28 | font-size: 10px; 29 | margin: 0 0 20px 0; 30 | user-select: none; 31 | } 32 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/PageProgress.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | @if (PageProgressService?.IsProgressVisible == true) 4 | { 5 |
6 | } 7 | 8 | @code { 9 | [Inject] 10 | private PageProgressService PageProgressService { get; set; } = default!; 11 | 12 | protected override void OnParametersSet() 13 | { 14 | base.OnParametersSet(); 15 | if (PageProgressService != null) 16 | { 17 | PageProgressService.PropertyChanged += (_, _) => InvokeAsync(StateHasChanged); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/PageProgress.razor.css: -------------------------------------------------------------------------------- 1 | .progress-line, .progress-line:before { 2 | height: 3px; 3 | width: 100%; 4 | margin: 0; 5 | position: absolute; 6 | } 7 | .progress-line { 8 | background-color: var(--gtlv-page-progress-track); 9 | display: -webkit-flex; 10 | display: flex; 11 | } 12 | .progress-line:before { 13 | background-color: var(--gtlv-page-progress-rail); 14 | content: ''; 15 | -webkit-animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; 16 | animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; 17 | } 18 | @-webkit-keyframes running-progress { 19 | 0% { margin-left: 0px; margin-right: 100%; } 20 | 50% { margin-left: 25%; margin-right: 0%; } 21 | 100% { margin-left: 100%; margin-right: 0; } 22 | } 23 | @keyframes running-progress { 24 | 0% { margin-left: 0px; margin-right: 100%; } 25 | 50% { margin-left: 25%; margin-right: 0%; } 26 | 100% { margin-left: 100%; margin-right: 0; } 27 | } 28 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Revisions/RevisionList.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | @using AntDesign.TableModels 3 | @using static GitTimelapseView.RazorApp 4 | 5 | 6 | 7 |
8 | Git Revision List 9 |
10 |
11 | 12 | 22 | 23 | 24 | @context.Revision.Label 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 | 63 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Revisions/RevisionList.razor.cs: -------------------------------------------------------------------------------- 1 | using AntDesign.TableModels; 2 | using GitTimelapseView.Actions; 3 | using GitTimelapseView.Core.Models; 4 | using GitTimelapseView.Helpers; 5 | using GitTimelapseView.Services; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Web; 8 | 9 | namespace GitTimelapseView; 10 | 11 | public partial class RevisionList 12 | { 13 | private const string RevisionTooltip = "Click to change current revision"; 14 | private FileRevisionTableRow[] Revisions => TimelapseService.FileHistory != null ? TimelapseService.FileHistory.Revisions.Reverse().Select(x => new FileRevisionTableRow(x, TimelapseService)).ToArray() : []; 15 | 16 | [Inject] private TimelapseService TimelapseService { get; set; } = default!; 17 | 18 | protected override Task OnInitializedAsync() 19 | { 20 | TimelapseService.FileLoaded += async (_, _) => await RefreshAsync(); 21 | TimelapseService.CurrentFileRevisionIndexChanging += async (_, _) => await RefreshAsync(); 22 | return base.OnInitializedAsync(); 23 | } 24 | 25 | private Dictionary OnRow(RowData row) 26 | { 27 | void OnClick(MouseEventArgs args) 28 | { 29 | _ = new SelectCommitAction(row.Data.Revision.Commit).ExecuteAsync().ConfigureAwait(false); 30 | } 31 | 32 | void OnDoubleClick(MouseEventArgs args) 33 | { 34 | _ = new ChangeCurrentRevisionAction(row.Data.Index).ExecuteAsync().ConfigureAwait(false); 35 | } 36 | 37 | return new Dictionary { { "ondoubleclick", (Action)OnDoubleClick }, { "onclick", (Action)OnClick }, }; 38 | } 39 | 40 | public Task RefreshAsync() 41 | { 42 | return InvokeAsync(StateHasChanged); 43 | } 44 | 45 | public void OnNavigate(FileRevisionTableRow fileChangeRow) 46 | { 47 | _ = new ChangeCurrentRevisionAction(fileChangeRow.Index).ExecuteAsync().ConfigureAwait(false); 48 | } 49 | 50 | public class FileRevisionTableRow 51 | { 52 | private readonly TimelapseService _timelapseService; 53 | 54 | public FileRevisionTableRow(FileRevision revision, TimelapseService timelapseService) 55 | { 56 | Index = revision.Index; 57 | Id = revision.Commit.Id; 58 | ShortId = revision.Commit.ShortId; 59 | Message = revision.Commit.Message; 60 | AuthorDate = revision.Commit.Committer.When.LocalDateTime; 61 | AuthorEmail = revision.Commit.Author.Email; 62 | Revision = revision; 63 | _timelapseService = timelapseService; 64 | } 65 | 66 | public int Index { get; set; } 67 | 68 | public string Id { get; set; } 69 | 70 | public string ShortId { get; set; } 71 | 72 | public string Message { get; set; } 73 | 74 | public DateTime AuthorDate { get; set; } 75 | 76 | public string AuthorEmail { get; set; } 77 | 78 | public string RowClass => _timelapseService.CurrentFileRevisionIndex == Index ? "current-revision revision" : 79 | _timelapseService.CurrentFileRevisionIndex < Index ? "future-revision revision" : "past-revision revision"; 80 | 81 | public FileRevision Revision { get; } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Revisions/RevisionList.razor.css: -------------------------------------------------------------------------------- 1 | .current-revision { 2 | background-color: var(--gtlv-editor-highlight); 3 | } 4 | 5 | .future-revision { 6 | opacity: 0.3; 7 | } 8 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/ThemeProvider.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | 4 | @if (ThemingService?.Theme?.IsDark == true) 5 | { 6 | 7 | 8 | } 9 | else 10 | { 11 | 12 | 13 | } 14 | 15 | 16 | 17 | 18 | 19 | 20 | @code { 21 | [Inject] 22 | ThemingService ThemingService { get; set; } = default!; 23 | } 24 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/RevisionTooltip.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | 4 | 5 |
6 | @if (Revision != null) 7 | { 8 | @Revision.Commit.MessageShort
9 | @Revision.Commit.Author.Name
10 | @Revision.Commit.Author.When
11 | @Revision.Commit.ShortId
12 | } 13 |
14 |
15 | 16 | @ChildContent 17 | 18 |
19 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/RevisionTooltip.razor.cs: -------------------------------------------------------------------------------- 1 | using AntDesign; 2 | using GitTimelapseView.Core.Models; 3 | using GitTimelapseView.Services; 4 | using Microsoft.AspNetCore.Components; 5 | 6 | namespace GitTimelapseView; 7 | 8 | public partial class RevisionTooltip 9 | { 10 | [Inject] public TimelapseService? TimelapseService { get; set; } = default!; 11 | 12 | [Parameter] public FileRevision Revision { get; set; } = default!; 13 | 14 | [Parameter] public RenderFragment ChildContent { get; set; } = default!; 15 | 16 | [Parameter] public Placement Placement { get; set; } = Placement.Top; 17 | private string Title => $"Revision {Revision.Label}"; 18 | } 19 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/RevisionTooltip.razor.css: -------------------------------------------------------------------------------- 1 | .mdi { 2 | margin-right: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/UserTooltip.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | 4 | 5 | 6 | @Title 7 | 8 | 9 |
10 | @if (!string.IsNullOrEmpty(UserInfo.Title)) 11 | { 12 | @UserInfo.Title
13 | } 14 | @if (!string.IsNullOrEmpty(UserInfo.Project)) 15 | { 16 | @UserInfo.Project
17 | } 18 | @if (!string.IsNullOrEmpty(UserInfo.Email)) 19 | { 20 | @UserInfo.Email
21 | } 22 | @if (!string.IsNullOrEmpty(UserInfo.Location)) 23 | { 24 | @UserInfo.Location
25 | } 26 |
27 |
28 | 29 | @ChildContent 30 | 31 |
32 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/UserTooltip.razor.cs: -------------------------------------------------------------------------------- 1 | using AntDesign; 2 | using GitTimelapseView.Extensions; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace GitTimelapseView; 6 | 7 | public partial class UserTooltip 8 | { 9 | [Parameter] public UserInfo UserInfo { get; set; } = default!; 10 | 11 | [Parameter] public RenderFragment ChildContent { get; set; } = default!; 12 | 13 | [Parameter] public Placement Placement { get; set; } = AntDesign.Placement.Top; 14 | 15 | private string Title 16 | { 17 | get 18 | { 19 | if (!string.IsNullOrEmpty(UserInfo.DisplayName)) 20 | { 21 | return UserInfo.DisplayName; 22 | } 23 | 24 | if (!string.IsNullOrEmpty(UserInfo.AccountName)) 25 | { 26 | return UserInfo.AccountName; 27 | } 28 | 29 | return UserInfo.Email; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GitTimelapseView/Components/Tooltips/UserTooltip.razor.css: -------------------------------------------------------------------------------- 1 | .mdi { 2 | margin-right: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /GitTimelapseView/Data/CommitChangeReason.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Data 5 | { 6 | public enum CommitChangeReason 7 | { 8 | /// 9 | /// Explicitely triggered by user 10 | /// 11 | Explicit, 12 | 13 | /// 14 | /// Triggered during the loading 15 | /// 16 | Loading, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GitTimelapseView/Data/CommitChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Core.Models; 5 | 6 | namespace GitTimelapseView.Data 7 | { 8 | public class CommitChangedEventArgs : EventArgs 9 | { 10 | public CommitChangedEventArgs(Commit commit, CommitChangeReason reason) 11 | { 12 | Commit = commit; 13 | Reason = reason; 14 | } 15 | 16 | public Commit Commit { get; set; } 17 | 18 | public CommitChangeReason Reason { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GitTimelapseView/Data/FileRevisionIndexChangeReason.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Data 5 | { 6 | public enum FileRevisionIndexChangeReason 7 | { 8 | /// 9 | /// Explicitely triggered by the user 10 | /// 11 | Explicit, 12 | 13 | /// 14 | /// Triggered during loading 15 | /// 16 | Loading, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GitTimelapseView/Data/FileRevisionIndexChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Data 5 | { 6 | public class FileRevisionIndexChangedEventArgs : EventArgs 7 | { 8 | public FileRevisionIndexChangedEventArgs(int index, FileRevisionIndexChangeReason reason) 9 | { 10 | Index = index; 11 | Reason = reason; 12 | } 13 | 14 | public int Index { get; set; } 15 | 16 | public FileRevisionIndexChangeReason Reason { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GitTimelapseView/GitTimelapseView.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | true 6 | Wpf\Resources\appicon.ico 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | PreserveNewest 59 | 60 | 61 | 62 | 63 | 64 | True 65 | True 66 | Settings.settings 67 | 68 | 69 | 70 | 71 | 72 | SettingsSingleFileGenerator 73 | Settings.Designer.cs 74 | 75 | 76 | 77 | 78 | 79 | all 80 | runtime; build; native; contentfiles; analyzers; buildtransitive 81 | 82 | 83 | all 84 | runtime; build; native; contentfiles; analyzers; buildtransitive 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /GitTimelapseView/GitTimelapseView.facade.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /GitTimelapseView/GitTimelapseView.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GitTimelapseView/Helpers/ActionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using GitTimelapseView.Services; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace GitTimelapseView.Helpers 9 | { 10 | public static class ActionExtensions 11 | { 12 | public static Task ExecuteAsync(this IAction action) 13 | { 14 | return App.Current.ServiceProvider.GetService()?.ExecuteAsync(action) ?? Task.CompletedTask; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GitTimelapseView/Helpers/BindableBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.ComponentModel; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace GitTimelapseView.Helpers 8 | { 9 | public class BindableBase : INotifyPropertyChanged 10 | { 11 | public event PropertyChangedEventHandler? PropertyChanged; 12 | 13 | public void ObservePropertyChanged(string propertyName, EventHandler handler, bool addOrRemove = true) 14 | { 15 | if (addOrRemove) 16 | { 17 | PropertyChangedEventManager.AddHandler(this, handler, propertyName); 18 | } 19 | else 20 | { 21 | PropertyChangedEventManager.RemoveHandler(this, handler, propertyName); 22 | } 23 | } 24 | 25 | protected virtual bool SetProperty(ref T storage, T value, [CallerMemberName] string? propertyName = null) 26 | { 27 | if (EqualityComparer.Default.Equals(storage, value)) return false; 28 | 29 | storage = value; 30 | RaisePropertyChanged(propertyName); 31 | 32 | return true; 33 | } 34 | 35 | protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) 36 | { 37 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GitTimelapseView/Helpers/DependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace GitTimelapseView.Helpers 8 | { 9 | internal static class DependencyInjectionExtensions 10 | { 11 | public static void RegisterService(this ServiceCollection serviceCollection, TService service) 12 | where TService : class, IService 13 | { 14 | serviceCollection.AddSingleton(service); 15 | serviceCollection.AddSingleton(service); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GitTimelapseView/Helpers/FileExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Helpers; 5 | 6 | public static class FileExtensions 7 | { 8 | static FileExtensions() 9 | { 10 | ExtensionsToLanguage = new Dictionary 11 | { 12 | { ".js", "javascript" }, 13 | { ".ts", "typescript" }, 14 | { ".cs", "csharp" }, 15 | { ".vb", "vb" }, 16 | { ".html", "html" }, 17 | { ".htm", "html" }, 18 | { ".css", "css" }, 19 | { ".scss", "scss" }, 20 | { ".json", "json" }, 21 | { ".xml", "xml" }, 22 | { ".yaml", "yaml" }, 23 | { ".yml", "yaml" }, 24 | { ".md", "markdown" }, 25 | { ".py", "python" }, 26 | { ".java", "java" }, 27 | { ".cpp", "cpp" }, 28 | { ".c", "c" }, 29 | { ".h", "c" }, 30 | { ".hpp", "cpp" }, 31 | { ".cc", "cpp" }, 32 | { ".go", "go" }, 33 | { ".rs", "rust" }, 34 | { ".swift", "swift" }, 35 | { ".kt", "kotlin" }, 36 | { ".kts", "kotlin" }, 37 | { ".php", "php" }, 38 | { ".rb", "ruby" }, 39 | { ".pl", "perl" }, 40 | { ".sh", "shell" }, 41 | { ".bat", "bat" }, 42 | { ".ps1", "powershell" }, 43 | { ".lua", "lua" }, 44 | { ".r", "r" }, 45 | { ".dart", "dart" }, 46 | { ".sql", "sql" }, 47 | { ".graphql", "graphql" }, 48 | { ".ini", "ini" }, 49 | { ".toml", "toml" }, 50 | { ".cfg", "ini" }, 51 | { ".tex", "latex" }, 52 | { ".jsx", "javascript" }, 53 | { ".tsx", "typescript" }, 54 | { ".svelte", "svelte" }, 55 | { ".vue", "vue" }, 56 | { ".razor", "razor" }, 57 | { ".aspx", "razor" }, 58 | { ".cshtml", "razor" }, 59 | { ".handlebars", "handlebars" }, 60 | { ".hbs", "handlebars" }, 61 | { ".ejs", "html" }, 62 | }; 63 | } 64 | 65 | public static IDictionary ExtensionsToLanguage { get; } 66 | } 67 | -------------------------------------------------------------------------------- /GitTimelapseView/Helpers/NotificationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using AntDesign; 5 | 6 | namespace GitTimelapseView.Helpers 7 | { 8 | internal static class NotificationServiceExtensions 9 | { 10 | public static Task ShowSuccess(this NotificationService notificationService, string message) 11 | { 12 | return notificationService.Open(new NotificationConfig 13 | { 14 | Message = message, 15 | NotificationType = NotificationType.Success, 16 | }); 17 | } 18 | 19 | public static Task ShowError(this NotificationService notificationService, string message) 20 | { 21 | return notificationService.Open(new NotificationConfig 22 | { 23 | Message = message, 24 | NotificationType = NotificationType.Error, 25 | }); 26 | } 27 | 28 | public static Task ShowInfo(this NotificationService notificationService, string message) 29 | { 30 | return notificationService.Open(new NotificationConfig 31 | { 32 | Message = message, 33 | NotificationType = NotificationType.Info, 34 | }); 35 | } 36 | 37 | public static Task ShowWarning(this NotificationService notificationService, string message) 38 | { 39 | return notificationService.Open(new NotificationConfig 40 | { 41 | Message = message, 42 | NotificationType = NotificationType.Warning, 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GitTimelapseView/Layouts/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @namespace GitTimelapseView 3 | 4 | 5 | 6 |
7 | 8 | @Body 9 |
10 | 11 | @code{ 12 | [Inject] 13 | TelemetryService TelemetryService { get; set; } = default!; 14 | 15 | private void OnMouseDown() 16 | { 17 | NotifySessionTrackerUserInput(); 18 | } 19 | 20 | private void OnMouseWheel() 21 | { 22 | NotifySessionTrackerUserInput(); 23 | } 24 | 25 | private void OnKeyDown() 26 | { 27 | NotifySessionTrackerUserInput(); 28 | } 29 | 30 | private void NotifySessionTrackerUserInput() 31 | { 32 | if (TelemetryService != null) 33 | { 34 | TelemetryService.NotifyUserActivity(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GitTimelapseView/Layouts/MainLayout.razor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/Layouts/MainLayout.razor.css -------------------------------------------------------------------------------- /GitTimelapseView/Pages/ErrorPage.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 | 7 | 8 | 9 | 10 | 11 | @if (@Context.Exception != null) 12 | { 13 |
14 | 15 | 16 | @Context.Exception.GetType().Name : @Context.Exception.Message 17 | 18 | 19 | 20 | @Context.Exception.StackTrace?.ToString() 21 | 22 |
23 | } 24 |
25 |
26 | 27 | @code{ 28 | [Parameter] 29 | public IActionContext Context { get; set; } = default!; 30 | 31 | private void OnOpenClicked() 32 | { 33 | new OpenFileAction().ExecuteAsync().ConfigureAwait(false); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/ErrorPage.razor.css: -------------------------------------------------------------------------------- 1 | 2 | .loading-component { 3 | background: #007BFF; 4 | margin-top: 100px; 5 | margin-left: 10px; 6 | margin-right: 10px; 7 | border-radius: 7px; 8 | padding: 50px 20px 120px 20px; 9 | } 10 | 11 | .title { 12 | font-size: 54px; 13 | font-weight: 300; 14 | line-height: 1.2; 15 | color: white; 16 | margin-top: 30px; 17 | margin-bottom: .1em; 18 | } 19 | 20 | .subtitle { 21 | font-size: 1.25rem; 22 | font-weight: 300; 23 | color: white; 24 | } 25 | 26 | .separator { 27 | border-bottom: 1px solid #999999; 28 | margin-top: 20px; 29 | margin-bottom: 20px; 30 | } 31 | 32 | .spinner { 33 | margin-left: 20px; 34 | } 35 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/GettingStartedPage.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 |
4 |

Getting Started

5 |

6 | Click on to view history of any file from a git repository 7 |

8 |
9 |
10 | 11 | 12 | @code { 13 | private void OnOpenClicked() 14 | { 15 | new OpenFileAction().ExecuteAsync().ConfigureAwait(false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/GettingStartedPage.razor.css: -------------------------------------------------------------------------------- 1 | .getting-started-component { 2 | background: var(--gtlv-card-header-background); 3 | margin-top: 100px; 4 | margin-left: 10px; 5 | margin-right: 10px; 6 | border-radius: 7px; 7 | padding: 50px 20px 120px 20px; 8 | color: var(--gtlv-foreground); 9 | } 10 | 11 | .title { 12 | font-size: 54px; 13 | font-weight: 300; 14 | line-height: 1.2; 15 | margin-top: 30px; 16 | margin-bottom: .1em; 17 | } 18 | 19 | .subtitle { 20 | font-size: 1.25rem; 21 | font-weight: 300; 22 | } 23 | 24 | .separator { 25 | border-bottom: 1px solid #999999; 26 | margin-top: 20px; 27 | margin-bottom: 20px; 28 | } 29 | 30 | .mdi { 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/LoadingPage.razor: -------------------------------------------------------------------------------- 1 | @namespace GitTimelapseView 2 | 3 |
4 |

Loading

5 |

6 | @Text 7 |

8 |
9 |
10 |
11 | 12 | @code { 13 | [Parameter] 14 | public string Text { get; set; } = default!; 15 | } 16 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/LoadingPage.razor.css: -------------------------------------------------------------------------------- 1 | 2 | .loading-component { 3 | background: #007BFF; 4 | margin-top: 100px; 5 | margin-left: 10px; 6 | margin-right: 10px; 7 | border-radius: 7px; 8 | padding: 50px 20px 120px 20px; 9 | } 10 | 11 | .title { 12 | font-size: 54px; 13 | font-weight: 300; 14 | line-height: 1.2; 15 | color: white; 16 | margin-top: 30px; 17 | margin-bottom: .1em; 18 | } 19 | 20 | .subtitle { 21 | font-size: 1.25rem; 22 | font-weight: 300; 23 | color: white; 24 | } 25 | 26 | .separator { 27 | border-bottom: 1px solid #999999; 28 | margin-top: 20px; 29 | margin-bottom: 20px; 30 | } 31 | 32 | .spinner { 33 | margin-left: 20px; 34 | } 35 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/MainPage.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using System.Timers 3 | 4 | @if (_actionContext != null && _actionContext.State == ActionState.Failed && _actionContext.ErrorFeedback == VisualFeedback.FullScreen) 5 | { 6 | 7 | } 8 | else if (_actionContext != null && _actionContext.State == ActionState.Running && _actionContext.ProgressFeedback == VisualFeedback.FullScreen) 9 | { 10 | 11 | } 12 | else if (TimelapseService.FileHistory == null) 13 | { 14 | 15 | } 16 | else 17 | { 18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 | } 36 | 37 | @code { 38 | [Inject] 39 | TimelapseService TimelapseService { get; set; } = default!; 40 | 41 | [Inject] 42 | ActionService ActionService { get; set; } = default!; 43 | 44 | [Inject] 45 | MessagingService MessagingService { get; set; } = default!; 46 | 47 | [Inject] 48 | MessageService MessageService { get; set; } = default!; 49 | 50 | IActionContext? _actionContext; 51 | 52 | protected override async Task OnInitializedAsync() 53 | { 54 | ActionService.ActionStarted += (_, args) => OnActionStarted(args); 55 | ActionService.ActionCompleted += (_, args) => OnActionCompleted(args); 56 | MessagingService.MessageService = MessageService; 57 | if (ActionService.CurrentAction != null) 58 | { 59 | OnActionStarted(ActionService.CurrentAction); 60 | } 61 | await base.OnInitializedAsync(); 62 | } 63 | 64 | private void OnActionStarted(IActionContext actionContext) 65 | { 66 | _actionContext = actionContext; 67 | ((BindableBase)actionContext).ObservePropertyChanged(nameof(IActionContext.ProgressFeedback), (_, _) => UpdateProgressFeedback()); 68 | UpdateProgressFeedback(); 69 | } 70 | 71 | private void OnActionCompleted(IActionContext actionContext) 72 | { 73 | UpdateProgressFeedback(); 74 | } 75 | 76 | void UpdateProgressFeedback() 77 | { 78 | RefreshAsync().ConfigureAwait(false); 79 | } 80 | 81 | private async Task RefreshAsync() 82 | { 83 | await InvokeAsync(() => StateHasChanged()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /GitTimelapseView/Pages/MainPage.razor.css: -------------------------------------------------------------------------------- 1 | .main-grid { 2 | display: grid; 3 | grid-template-rows: auto 1fr 350px; 4 | grid-template-columns: 1fr 1fr; 5 | row-gap: 10px; 6 | column-gap: 10px; 7 | height: 100vh; 8 | overflow: hidden; 9 | padding: 10px; 10 | } 11 | 12 | .slider-box { 13 | grid-row: 1; 14 | grid-column: 1 / 3; 15 | } 16 | 17 | .text-editor-box { 18 | grid-row: 2; 19 | grid-column: 1 / 3; 20 | overflow: scroll; 21 | } 22 | 23 | .revision-list-box { 24 | grid-row: 3; 25 | grid-column: 1; 26 | } 27 | 28 | .commit-info-box { 29 | grid-row: 3; 30 | grid-column: 2; 31 | } 32 | -------------------------------------------------------------------------------- /GitTimelapseView/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace GitTimelapseView.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.0.3.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("-1")] 29 | public double Top { 30 | get { 31 | return ((double)(this["Top"])); 32 | } 33 | set { 34 | this["Top"] = value; 35 | } 36 | } 37 | 38 | [global::System.Configuration.UserScopedSettingAttribute()] 39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 40 | [global::System.Configuration.DefaultSettingValueAttribute("-1")] 41 | public double Left { 42 | get { 43 | return ((double)(this["Left"])); 44 | } 45 | set { 46 | this["Left"] = value; 47 | } 48 | } 49 | 50 | [global::System.Configuration.UserScopedSettingAttribute()] 51 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 52 | [global::System.Configuration.DefaultSettingValueAttribute("-1")] 53 | public double Height { 54 | get { 55 | return ((double)(this["Height"])); 56 | } 57 | set { 58 | this["Height"] = value; 59 | } 60 | } 61 | 62 | [global::System.Configuration.UserScopedSettingAttribute()] 63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 64 | [global::System.Configuration.DefaultSettingValueAttribute("-1")] 65 | public double Width { 66 | get { 67 | return ((double)(this["Width"])); 68 | } 69 | set { 70 | this["Width"] = value; 71 | } 72 | } 73 | 74 | [global::System.Configuration.UserScopedSettingAttribute()] 75 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 76 | [global::System.Configuration.DefaultSettingValueAttribute("False")] 77 | public bool Maximized { 78 | get { 79 | return ((bool)(this["Maximized"])); 80 | } 81 | set { 82 | this["Maximized"] = value; 83 | } 84 | } 85 | 86 | [global::System.Configuration.UserScopedSettingAttribute()] 87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 88 | [global::System.Configuration.DefaultSettingValueAttribute("")] 89 | public string Theme { 90 | get { 91 | return ((string)(this["Theme"])); 92 | } 93 | set { 94 | this["Theme"] = value; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /GitTimelapseView/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -1 7 | 8 | 9 | -1 10 | 11 | 12 | -1 13 | 14 | 15 | -1 16 | 17 | 18 | False 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /GitTimelapseView/RazorApp.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/ActionService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using GitTimelapseView.Helpers; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Services 9 | { 10 | public class ActionService : ServiceBase 11 | { 12 | private readonly ILoggerFactory _loggerFactory; 13 | private readonly ILogger _logger; 14 | private readonly TelemetryService _telemetryService; 15 | private readonly PageProgressService _pageProgressService; 16 | private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); 17 | private ActionContext? _currentAction; 18 | 19 | public ActionService(ILoggerFactory loggerFactory, TelemetryService telemetryService, MessagingService messagingService, PageProgressService pageProgressService) 20 | : base(loggerFactory) 21 | { 22 | _loggerFactory = loggerFactory; 23 | _logger = loggerFactory.CreateLogger(nameof(ActionService)); 24 | _telemetryService = telemetryService; 25 | MessagingService = messagingService; 26 | _pageProgressService = pageProgressService; 27 | } 28 | 29 | public event EventHandler? ActionStarted; 30 | 31 | public event EventHandler? ActionCompleted; 32 | 33 | public ActionContext? CurrentAction 34 | { 35 | get => _currentAction; 36 | set => SetProperty(ref _currentAction, value); 37 | } 38 | 39 | public MessagingService MessagingService { get; } 40 | 41 | public async Task ExecuteAsync(IAction action) 42 | { 43 | await _semaphoreSlim.WaitAsync().ConfigureAwait(false); 44 | var stopwatch = Stopwatch.StartNew(); 45 | var context = new ActionContext(_loggerFactory, action); 46 | context.ObservePropertyChanged(nameof(IActionContext.ProgressFeedback), (_, _) => _pageProgressService.IsProgressVisible = context.ProgressFeedback == VisualFeedback.ProgressBarTop); 47 | context.State = ActionState.Running; 48 | CurrentAction = context; 49 | ActionStarted?.Invoke(this, context); 50 | if (context.ProgressFeedback == VisualFeedback.ProgressBarTop) 51 | { 52 | _pageProgressService.IsProgressVisible = true; 53 | } 54 | 55 | try 56 | { 57 | await action.ExecuteAsync(context).ConfigureAwait(false); 58 | if (action.SuccessNotificationText != null) 59 | { 60 | MessagingService.Success(action.SuccessNotificationText); 61 | } 62 | 63 | context.State = ActionState.Success; 64 | } 65 | catch (Exception e) 66 | { 67 | context.ErrorMessage ??= $"Unable to perform action {action.GetType().Name}"; 68 | context.Exception = e; 69 | context.State = ActionState.Failed; 70 | Logger.LogError(e, $"Unable to perform action {action.GetType().Name}"); 71 | if (context.ErrorFeedback == VisualFeedback.Message) 72 | { 73 | MessagingService.Error(action.FailureNotificationText != null ? $"{action.FailureNotificationText}. {e}" : $"Unable to perform action {action.GetType().Name}. {e}"); 74 | } 75 | 76 | _telemetryService.TrackException(e); 77 | } 78 | 79 | if (context.IsTrackingEnabled) 80 | { 81 | context.TrackingProperties["ActionStatusText"] = context.State == ActionState.Success ? "Ok" : "Error"; 82 | context.TrackingMetrics["ActionExecutionTime"] = stopwatch.ElapsedMilliseconds; 83 | _telemetryService.TrackEvent(context.TrackingId, context.TrackingProperties, context.TrackingMetrics); 84 | } 85 | 86 | context.ProgressMessage = null; 87 | _pageProgressService.IsProgressVisible = false; 88 | 89 | CurrentAction = null; 90 | ActionCompleted?.Invoke(this, context); 91 | _semaphoreSlim.Release(); 92 | } 93 | 94 | public class ActionContext : BindableBase, IActionContext 95 | { 96 | private readonly ILogger _logger; 97 | private VisualFeedback _progressFeedback = VisualFeedback.ProgressBarTop; 98 | 99 | public ActionContext(ILoggerFactory loggerFactory, IAction action) 100 | { 101 | _logger = loggerFactory.CreateLogger(action.GetType().Name); 102 | TrackingId = action.GetType().Name.Replace("Action", string.Empty, StringComparison.Ordinal); 103 | Action = action; 104 | ProgressMessage = $"'{action.GetType().Name}' in progress"; 105 | } 106 | 107 | public string TrackingId { get; set; } 108 | 109 | public IDictionary TrackingProperties { get; } = new Dictionary(); 110 | 111 | public IDictionary TrackingMetrics { get; } = new Dictionary(); 112 | 113 | public bool IsTrackingEnabled { get; set; } = true; 114 | 115 | public IAction Action { get; } 116 | 117 | public VisualFeedback ProgressFeedback 118 | { 119 | get => _progressFeedback; 120 | set => SetProperty(ref _progressFeedback, value); 121 | } 122 | 123 | public string? ErrorMessage { get; set; } 124 | 125 | public string? ProgressMessage { get; set; } 126 | 127 | public Exception? Exception { get; set; } 128 | 129 | public VisualFeedback ErrorFeedback { get; set; } = VisualFeedback.Message; 130 | 131 | public ActionState State { get; set; } = ActionState.Unknown; 132 | 133 | public IDisposable? BeginScope(TState state) 134 | where TState : notnull 135 | { 136 | return _logger.BeginScope(state); 137 | } 138 | 139 | public bool IsEnabled(LogLevel logLevel) 140 | { 141 | return _logger.IsEnabled(logLevel); 142 | } 143 | 144 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 145 | { 146 | _logger.Log(logLevel, eventId, state, exception, formatter); 147 | if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) 148 | { 149 | var message = formatter(state, exception); 150 | ErrorMessage = message; 151 | Exception = exception; 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/MessagingService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Windows; 5 | using AntDesign; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Services 9 | { 10 | public class MessagingService : ServiceBase 11 | { 12 | public MessagingService(ILoggerFactory loggerFactory) 13 | : base(loggerFactory) 14 | { 15 | } 16 | 17 | internal MessageService? MessageService { get; set; } 18 | 19 | public void ShowInformationDialog(string errorMessage, string? title = null) 20 | { 21 | MessageBox.Show(errorMessage, title ?? App.Current.ApplicationName, MessageBoxButton.OK, MessageBoxImage.Information); 22 | } 23 | 24 | internal void Success(string message) 25 | { 26 | _ = MessageService?.Success(message); 27 | } 28 | 29 | internal void Error(string message) 30 | { 31 | _ = MessageService?.Error(message); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/PageProgressService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace GitTimelapseView.Services 7 | { 8 | public class PageProgressService : ServiceBase 9 | { 10 | private bool _isProgressVisible; 11 | 12 | public PageProgressService(ILoggerFactory loggerFactory) 13 | : base(loggerFactory) 14 | { 15 | } 16 | 17 | public bool IsProgressVisible 18 | { 19 | get => _isProgressVisible; 20 | set => SetProperty(ref _isProgressVisible, value); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/PluginService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.IO; 5 | using GitTimelapseView.Extensions; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace GitTimelapseView.Services 10 | { 11 | internal class PluginService 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public PluginService(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | internal void LoadPlugins(ServiceCollection serviceCollection) 21 | { 22 | var pluginsDirectory = Path.GetDirectoryName(typeof(PluginService).Assembly.Location); 23 | if (pluginsDirectory == null) 24 | { 25 | _logger.LogError($"Unable to load plugins"); 26 | return; 27 | } 28 | 29 | var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName?.Contains(nameof(GitTimelapseView), StringComparison.Ordinal) ?? false).ToList(); 30 | 31 | var matchingAssemblyFiles = Directory 32 | .EnumerateFiles(path: pluginsDirectory, searchPattern: $"{nameof(GitTimelapseView)}.*.dll", searchOption: SearchOption.AllDirectories) 33 | .GroupBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase).Select(f => f.First()); 34 | 35 | foreach (var assemblyFilePath in matchingAssemblyFiles) 36 | { 37 | var assemblyFileName = Path.GetFileName(assemblyFilePath); 38 | if (loadedAssemblies.All(a => !string.Equals(a.ManifestModule.Name, assemblyFileName, StringComparison.Ordinal))) 39 | _ = Assembly.LoadFrom(assemblyFilePath); 40 | } 41 | 42 | try 43 | { 44 | var pluginAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && IsPlugin(a)).ToList(); 45 | foreach (var assembly in pluginAssemblies) 46 | { 47 | LoadPlugin(assembly, serviceCollection); 48 | } 49 | } 50 | catch (Exception e) 51 | { 52 | _logger.LogError(e, $"Unable to load plugins"); 53 | } 54 | } 55 | 56 | private static bool IsPlugin(Assembly assembly) 57 | { 58 | return assembly.ExportedTypes.Any(x => typeof(IPlugin).IsAssignableFrom(x) && !x.IsAbstract); 59 | } 60 | 61 | private void LoadPlugin(Assembly assembly, ServiceCollection serviceCollection) 62 | { 63 | var pluginClasses = assembly.GetExportedTypes().Where(x => typeof(IPlugin).IsAssignableFrom(x) && !x.IsAbstract).ToArray(); 64 | if (pluginClasses.Length == 1) 65 | { 66 | try 67 | { 68 | var plugin = Activator.CreateInstance(pluginClasses[0]) as IPlugin; 69 | if (plugin == null) 70 | { 71 | _logger.LogError($"Unable to instantiate plugin '{assembly.GetName().Name}'"); 72 | return; 73 | } 74 | 75 | plugin.ConfigureServices(serviceCollection); 76 | _logger.LogInformation($"Plugin '{assembly.GetName().Name}' Loaded"); 77 | } 78 | catch (Exception e) 79 | { 80 | _logger.LogError(e, $"Unable to load plugin '{assembly.GetName().Name}'"); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/ServiceBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using GitTimelapseView.Helpers; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Services 9 | { 10 | public abstract class ServiceBase : BindableBase, IService 11 | { 12 | protected ServiceBase(ILoggerFactory loggerFactory) 13 | { 14 | LoggerFactory = loggerFactory; 15 | Logger = loggerFactory.CreateLogger(GetType().Name); 16 | } 17 | 18 | public ILoggerFactory LoggerFactory { get; } 19 | 20 | public ILogger Logger { get; } 21 | 22 | public virtual Task InitializeAsync() 23 | { 24 | return Task.CompletedTask; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/TelemetryService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Extensions; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace GitTimelapseView.Services 8 | { 9 | public class TelemetryService : ServiceBase 10 | { 11 | private readonly Lazy> _providers; 12 | private readonly IAppInfo _appInfo; 13 | 14 | public TelemetryService(ILoggerFactory loggerFactory, Lazy> providers, IAppInfo appInfo) 15 | : base(loggerFactory) 16 | { 17 | _providers = providers; 18 | _appInfo = appInfo; 19 | } 20 | 21 | public override async Task InitializeAsync() 22 | { 23 | foreach (var provider in _providers.Value) 24 | { 25 | await provider.InitializeAsync(Logger, _appInfo).ConfigureAwait(false); 26 | } 27 | } 28 | 29 | public void SetAdditionalInfo(string propertyName, object value) 30 | { 31 | foreach (var provider in _providers.Value) 32 | { 33 | provider.SetAdditionalInfo(propertyName, value); 34 | } 35 | } 36 | 37 | public void TrackPageView(string pageName, TimeSpan loadingTime = default, Uri? pageUrl = null) 38 | { 39 | foreach (var provider in _providers.Value) 40 | { 41 | provider.TrackPageView(pageName, loadingTime, pageUrl); 42 | } 43 | } 44 | 45 | public void TrackEvent(string name, IDictionary? properties = null, IDictionary? metrics = null) 46 | { 47 | foreach (var provider in _providers.Value) 48 | { 49 | provider.TrackEvent(name, properties, metrics); 50 | } 51 | } 52 | 53 | public void TrackMetric(string name, double value, IDictionary? properties = null) 54 | { 55 | foreach (var provider in _providers.Value) 56 | { 57 | provider.TrackMetric(name, value, properties); 58 | } 59 | } 60 | 61 | public void TrackException(Exception exception) 62 | { 63 | foreach (var provider in _providers.Value) 64 | { 65 | provider.TrackException(exception); 66 | } 67 | } 68 | 69 | public void NotifyUserActivity() 70 | { 71 | foreach (var provider in _providers.Value) 72 | { 73 | provider.NotifyUserActivity(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/ThemeInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | namespace GitTimelapseView.Services 5 | { 6 | public record ThemeInfo 7 | { 8 | public ThemeInfo(string name) 9 | { 10 | Name = name; 11 | } 12 | 13 | public string Name { get; } 14 | 15 | public string MonacoTheme { get; set; } = string.Empty; 16 | 17 | public bool IsDark { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/ThemingService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using MaterialDesignThemes.Wpf; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace GitTimelapseView.Services 8 | { 9 | public class ThemingService : ServiceBase 10 | { 11 | private readonly ThemeInfo _lightTheme = new("Light") 12 | { 13 | MonacoTheme = "vs", 14 | }; 15 | 16 | private readonly ThemeInfo _darkTheme = new("Dark") 17 | { 18 | IsDark = true, 19 | MonacoTheme = "vs-dark", 20 | }; 21 | 22 | public ThemingService(ILoggerFactory loggerFactory) 23 | : base(loggerFactory) 24 | { 25 | Themes = 26 | [ 27 | _lightTheme, 28 | _darkTheme 29 | ]; 30 | var themeProperty = Properties.Settings.Default.Theme; 31 | var theme = !string.IsNullOrEmpty(themeProperty) 32 | ? Themes.FirstOrDefault(x => x.Name.Equals(themeProperty, StringComparison.OrdinalIgnoreCase)) ?? Themes[0] 33 | : Themes[0]; 34 | 35 | Theme = theme; 36 | ApplyTheme(theme, saveSettings: false, reloadWindow: false); 37 | } 38 | 39 | public ThemeInfo Theme { get; private set; } 40 | 41 | public ThemeInfo[] Themes { get; } 42 | 43 | public void ApplyTheme(ThemeInfo themeInfo, bool saveSettings = true, bool reloadWindow = true) 44 | { 45 | Theme = themeInfo; 46 | if (saveSettings) 47 | { 48 | Properties.Settings.Default.Theme = themeInfo.Name; 49 | Properties.Settings.Default.Save(); 50 | } 51 | 52 | if (reloadWindow && App.Current.MainWindow is MainWindow mainWindow) 53 | { 54 | mainWindow.Reload(); 55 | } 56 | 57 | var paletteHelper = new PaletteHelper(); 58 | var theme = paletteHelper.GetTheme(); 59 | if (themeInfo.IsDark) 60 | { 61 | theme.SetDarkTheme(); 62 | } 63 | else 64 | { 65 | theme.SetLightTheme(); 66 | } 67 | 68 | paletteHelper.SetTheme(theme); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/TimelapseService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using GitTimelapseView.Core.Models; 5 | using GitTimelapseView.Data; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace GitTimelapseView.Services 9 | { 10 | public class TimelapseService : ServiceBase 11 | { 12 | private FileRevision? _currentFileRevision; 13 | private Commit? _currentCommit; 14 | private string? _filePath; 15 | 16 | public TimelapseService(ILoggerFactory loggerFactory) 17 | : base(loggerFactory) 18 | { 19 | CommitInfoSelectedTab = "message"; 20 | } 21 | 22 | public event EventHandler? FileLoading; 23 | 24 | public event EventHandler? FileLoaded; 25 | 26 | public event EventHandler? CurrentFileRevisionIndexChanged; 27 | 28 | public event EventHandler? CurrentFileRevisionIndexChanging; 29 | 30 | public event EventHandler? CurrentCommitChanging; 31 | 32 | public event EventHandler? CurrentCommitChanged; 33 | 34 | public string? FilePath 35 | { 36 | get => _filePath; 37 | private set => SetProperty(ref _filePath, value); 38 | } 39 | 40 | public FileHistory? FileHistory { get; private set; } 41 | 42 | public FileRevision? CurrentFileRevision 43 | { 44 | get => _currentFileRevision; 45 | private set => SetProperty(ref _currentFileRevision, value); 46 | } 47 | 48 | public int CurrentFileRevisionIndex => CurrentFileRevision?.Index ?? 0; 49 | 50 | public Commit? CurrentCommit 51 | { 52 | get => _currentCommit; 53 | private set => SetProperty(ref _currentCommit, value); 54 | } 55 | 56 | public int? InitialLineNumber { get; set; } 57 | 58 | public string CommitInfoSelectedTab { get; set; } 59 | 60 | public async Task OpenFileAsync(ILogger logger, string filePath, int? lineNumber, int? revisionIndex, string? revisionSha) 61 | { 62 | var oldCommitId = CurrentCommit; 63 | var oldFilePath = FilePath; 64 | try 65 | { 66 | FileLoading?.Invoke(this, filePath); 67 | CurrentCommit = null; 68 | CurrentFileRevision = null; 69 | FilePath = filePath; 70 | 71 | FileHistory = new FileHistory(filePath); 72 | await FileHistory.InitializeAsync(logger).ConfigureAwait(false); 73 | 74 | InitialLineNumber = lineNumber ?? 0; 75 | if (FileHistory.Revisions.Count > 0) 76 | { 77 | var revisionIndexToSelect = FileHistory.Revisions.Count - 1; 78 | if (revisionIndex != null && revisionIndex > 0 && revisionIndex < FileHistory.Revisions.Count) 79 | { 80 | revisionIndexToSelect = revisionIndex.Value; 81 | } 82 | else if (revisionSha != null) 83 | { 84 | var revision = FileHistory.Revisions.FirstOrDefault(x => string.Equals(x.Commit.Id, revisionSha, StringComparison.Ordinal)); 85 | if (revision != null) 86 | { 87 | revisionIndexToSelect = revision.Index; 88 | } 89 | } 90 | 91 | await SetCurrentFileRevisionIndexAsync(logger, revisionIndexToSelect, FileRevisionIndexChangeReason.Loading).ConfigureAwait(false); 92 | } 93 | 94 | FileLoaded?.Invoke(this, filePath); 95 | } 96 | catch (Exception e) 97 | { 98 | CurrentCommit = oldCommitId; 99 | FilePath = oldFilePath; 100 | logger.LogError(e, $"Unable to open file '{filePath}'"); 101 | throw; 102 | } 103 | } 104 | 105 | public async Task SetCurrentFileRevisionIndexAsync(ILogger logger, int index, FileRevisionIndexChangeReason reason = FileRevisionIndexChangeReason.Explicit) 106 | { 107 | if (FileHistory == null) 108 | { 109 | logger.LogError($"Unable to set current file revision index to {index}. No {nameof(FileHistory)}"); 110 | return; 111 | } 112 | 113 | try 114 | { 115 | CurrentFileRevisionIndexChanging?.Invoke(this, new FileRevisionIndexChangedEventArgs(index, reason)); 116 | CurrentFileRevision = FileHistory.Revisions[index]; 117 | await CurrentFileRevision.LoadBlocksAsync(logger).ConfigureAwait(false); 118 | CurrentCommit = CurrentFileRevision.Commit; 119 | await CurrentCommit.UpdateInfoAsync(logger).ConfigureAwait(false); 120 | CurrentFileRevisionIndexChanged?.Invoke(this, new FileRevisionIndexChangedEventArgs(index, reason)); 121 | } 122 | catch (Exception e) 123 | { 124 | logger.LogError(e, $"Unable to set current file revision index to {index}"); 125 | } 126 | } 127 | 128 | public async Task SetCurrentCommitAsync(ILogger logger, Commit commit, CommitChangeReason reason = CommitChangeReason.Explicit) 129 | { 130 | try 131 | { 132 | CurrentCommitChanging?.Invoke(this, new CommitChangedEventArgs(commit, reason)); 133 | CurrentCommit = commit; 134 | await commit.UpdateInfoAsync(logger).ConfigureAwait(false); 135 | CurrentCommitChanged?.Invoke(this, new CommitChangedEventArgs(commit, reason)); 136 | } 137 | catch (Exception e) 138 | { 139 | logger.LogError(e, $"Unable to set current commit to {commit.Id}"); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /GitTimelapseView/Services/UserInfoService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Globalization; 5 | using System.Text; 6 | using GitTimelapseView.Extensions; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace GitTimelapseView.Services 10 | { 11 | public class UserInfoService : ServiceBase 12 | { 13 | public UserInfoService(ILoggerFactory loggerFactory, Lazy> providers) 14 | : base(loggerFactory) 15 | { 16 | Providers = providers; 17 | } 18 | 19 | public Lazy> Providers { get; } 20 | 21 | public async Task GetUserInfoAsync(string email) 22 | { 23 | UserInfo? userInfo = null; 24 | foreach (var provider in Providers.Value) 25 | { 26 | try 27 | { 28 | userInfo = await provider.GetUserInfoAsync(email).ConfigureAwait(false); 29 | } 30 | catch (Exception e) 31 | { 32 | Logger.LogError(e, $"Unable to get user info from email '{email}' with provider '{provider.GetType().Name}'"); 33 | } 34 | 35 | if (userInfo != null) 36 | break; 37 | } 38 | 39 | if (userInfo == null) 40 | { 41 | userInfo = new UserInfo(email); 42 | } 43 | 44 | if (string.IsNullOrEmpty(userInfo.ProfilePictureUrl)) 45 | { 46 | userInfo.ProfilePictureUrl = GetGravatarProfileUrl(email); 47 | } 48 | 49 | return userInfo; 50 | } 51 | 52 | private static string GetGravatarProfileUrl(string email) 53 | { 54 | using var md5 = System.Security.Cryptography.MD5.Create(); 55 | var inputBytes = Encoding.ASCII.GetBytes(email.ToLowerInvariant().Trim(' ')); 56 | var hashBytes = md5.ComputeHash(inputBytes); 57 | 58 | var sb = new StringBuilder(); 59 | for (var i = 0; i < hashBytes.Length; i++) 60 | { 61 | sb.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", hashBytes[i]); 62 | } 63 | 64 | var hash = sb.ToString().ToLowerInvariant(); 65 | return $"https://www.gravatar.com/avatar/{hash}"; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GitTimelapseView/StartupOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using CommandLine; 5 | 6 | namespace GitTimelapseView 7 | { 8 | public class StartupOptions 9 | { 10 | [Option('i', "input", Required = false, HelpText = "Input file to be opened")] 11 | public string? InputFile { get; set; } 12 | 13 | [Option('l', "line", Required = false, HelpText = "Line number to select")] 14 | public int? LineNumber { get; set; } 15 | 16 | [Option('r', "rev", Required = false, HelpText = "Revision index to select")] 17 | public int? RevisionIndex { get; set; } 18 | 19 | [Option("sha", Required = false, HelpText = "Sha of the revision to select")] 20 | public string? Sha { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/App.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/App.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.IO; 5 | using System.Windows; 6 | using CommandLine; 7 | using GitTimelapseView.Actions; 8 | using GitTimelapseView.Extensions; 9 | using GitTimelapseView.Helpers; 10 | using GitTimelapseView.Services; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Logging.Abstractions; 14 | using Serilog; 15 | using Serilog.Events; 16 | using Application = System.Windows.Application; 17 | 18 | namespace GitTimelapseView 19 | { 20 | /// 21 | /// Interaction logic for App.xaml 22 | /// 23 | public partial class App : Application, IAppInfo 24 | { 25 | private static readonly TimeSpan s_maxLogAgeBeforeCleanup = TimeSpan.FromDays(14); 26 | private string? _version; 27 | 28 | public App() 29 | { 30 | Startup += async (sender, args) => await OnApplicationStartup(sender, args); 31 | } 32 | 33 | public static new App Current => (App)Application.Current; 34 | 35 | public ServiceProvider ServiceProvider { get; private set; } = new ServiceCollection().BuildServiceProvider(); 36 | 37 | public Microsoft.Extensions.Logging.ILogger Logger { get; private set; } = NullLogger.Instance; 38 | 39 | public ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance; 40 | 41 | public string ApplicationName 42 | { 43 | get 44 | { 45 | var asm = GetType().Assembly; 46 | var productName = asm.GetCustomAttribute()?.Product; 47 | if (!string.IsNullOrEmpty(productName)) 48 | { 49 | return productName; 50 | } 51 | 52 | return asm.GetName().Name ?? nameof(GitTimelapseView); 53 | } 54 | } 55 | 56 | public string ApplicationVersion 57 | { 58 | get 59 | { 60 | if (_version == null) 61 | { 62 | var asm = GetType().Assembly; 63 | _version = FileVersionInfo.GetVersionInfo(asm.Location).ProductVersion; 64 | } 65 | 66 | return _version ?? string.Empty; 67 | } 68 | } 69 | 70 | public string[] StartupArguments { get; private set; } = []; 71 | 72 | public StartupOptions StartupOptions { get; private set; } = new(); 73 | 74 | public string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ApplicationName); 75 | 76 | public string LogsPath => Path.Combine(ApplicationDataPath, "Logs"); 77 | 78 | private void ConfigureServices() 79 | { 80 | var serviceCollection = new ServiceCollection(); 81 | ConfigureLogger(serviceCollection); 82 | ConfigureAppInfo(serviceCollection); 83 | ConfigureBlazor(serviceCollection); 84 | ConfigureServices(serviceCollection); 85 | ConfigurePlugins(serviceCollection); 86 | ServiceProvider = serviceCollection.BuildServiceProvider(); 87 | } 88 | 89 | private async Task InitializeServicesAsync() 90 | { 91 | var services = ServiceProvider.GetServices(); 92 | foreach (var service in services) 93 | { 94 | await service.InitializeAsync(); 95 | } 96 | } 97 | 98 | private void ConfigureAppInfo(ServiceCollection serviceCollection) 99 | { 100 | Logger.LogInformation($"Initializing {ApplicationName} {ApplicationVersion}..."); 101 | if (StartupArguments != null && StartupArguments.Length > 0) 102 | { 103 | Logger.LogInformation($"with arguments: \"{string.Join(" ", StartupArguments)}\""); 104 | } 105 | 106 | serviceCollection.AddSingleton(this); 107 | } 108 | 109 | private void ConfigureBlazor(ServiceCollection serviceCollection) 110 | { 111 | serviceCollection 112 | .AddBlazorWebView() 113 | .AddAntDesign() 114 | .AddWpfBlazorWebView(); 115 | Logger.LogInformation("Blazor initialized"); 116 | } 117 | 118 | private void ConfigurePlugins(ServiceCollection serviceCollection) 119 | { 120 | var pluginService = new PluginService(LoggerFactory.CreateLogger(nameof(PluginService))); 121 | serviceCollection.AddSingleton(pluginService); 122 | pluginService.LoadPlugins(serviceCollection); 123 | Logger.LogInformation("Plugins initialized"); 124 | } 125 | 126 | private void ConfigureServices(ServiceCollection serviceCollection) 127 | { 128 | var timelapseService = new TimelapseService(LoggerFactory); 129 | var userInfoService = new UserInfoService(LoggerFactory, new Lazy>(() => ServiceProvider.GetServices())); 130 | var telemetryService = new TelemetryService(LoggerFactory, new Lazy>(() => ServiceProvider.GetServices()), this); 131 | var messagingService = new MessagingService(LoggerFactory); 132 | var pageProgressService = new PageProgressService(LoggerFactory); 133 | var actionService = new ActionService(LoggerFactory, telemetryService, messagingService, pageProgressService); 134 | var themingService = new ThemingService(LoggerFactory); 135 | serviceCollection.RegisterService(timelapseService); 136 | serviceCollection.RegisterService(userInfoService); 137 | serviceCollection.RegisterService(telemetryService); 138 | serviceCollection.RegisterService(actionService); 139 | serviceCollection.RegisterService(themingService); 140 | serviceCollection.RegisterService(messagingService); 141 | serviceCollection.RegisterService(pageProgressService); 142 | Logger.LogInformation("Services initialized"); 143 | } 144 | 145 | private void ConfigureLogger(ServiceCollection serviceCollection) 146 | { 147 | var outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}][{SourceContext}] {Message:lj}{NewLine}{Exception}"; 148 | 149 | var loggerConfiguration = new LoggerConfiguration() 150 | .Enrich.FromLogContext() 151 | .MinimumLevel.Is(LogEventLevel.Information) 152 | .WriteTo.File(Path.Combine(LogsPath, $"{ApplicationName}_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{System.Environment.ProcessId}.txt"), LogEventLevel.Information, outputTemplate: outputTemplate) 153 | .CreateLogger(); 154 | 155 | LoggerFactory = new LoggerFactory().AddSerilog(loggerConfiguration); 156 | serviceCollection.AddSingleton(LoggerFactory); 157 | 158 | Logger = LoggerFactory.CreateLogger("Default"); 159 | serviceCollection.AddSingleton(Logger); 160 | 161 | CleanOldLogs(); 162 | 163 | Logger.LogDebug("Logger initialized"); 164 | 165 | void CleanOldLogs() 166 | { 167 | try 168 | { 169 | var directory = new DirectoryInfo(LogsPath); 170 | var currentTime = DateTime.UtcNow; 171 | var logsToClean = directory.GetFiles("*.txt").Where(x => currentTime - x.LastWriteTime > s_maxLogAgeBeforeCleanup).ToArray(); 172 | if (logsToClean.Length > 0) 173 | { 174 | logsToClean.AsParallel().ForAll(x => File.Delete(x.FullName)); 175 | } 176 | } 177 | catch (Exception e) 178 | { 179 | Logger.LogError(e, "Unable to clean old log files"); 180 | } 181 | } 182 | } 183 | 184 | private async Task OnApplicationStartup(object sender, StartupEventArgs startupArguments) 185 | { 186 | StartupArguments = startupArguments.Args; 187 | Parser.Default.ParseArguments(StartupArguments) 188 | .WithParsed(o => { if (o != null) StartupOptions = o; }); 189 | 190 | if (StartupArguments != null && StartupArguments.Length == 1 && File.Exists(StartupArguments[0]) && string.IsNullOrEmpty(StartupOptions.InputFile)) 191 | { 192 | StartupOptions.InputFile = StartupArguments[0]; 193 | } 194 | 195 | ConfigureServices(); 196 | await InitializeServicesAsync(); 197 | 198 | var timelapseService = ServiceProvider.GetService(); 199 | var themingService = ServiceProvider.GetService(); 200 | var window = new MainWindow(timelapseService, themingService); 201 | window.Show(); 202 | Logger.LogInformation("> Application successfully initialized"); 203 | 204 | if (timelapseService != null && StartupOptions.InputFile != null) 205 | { 206 | await new OpenFileAction(StartupOptions.InputFile, StartupOptions.LineNumber, StartupOptions.RevisionIndex, StartupOptions.Sha).ExecuteAsync().ConfigureAwait(false); 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/Helpers/ActionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Media.Imaging; 7 | using GitTimelapseView.Extensions; 8 | using GitTimelapseView.Helpers; 9 | using MaterialDesignThemes.Wpf; 10 | 11 | namespace GitTimelapseView.Wpf.Helpers 12 | { 13 | internal static class ActionExtensions 14 | { 15 | public static Button ToButton(this IAction action) 16 | { 17 | var button = new Button 18 | { 19 | ToolTip = action.DisplayName, 20 | }; 21 | if (action.Children.Any()) 22 | { 23 | var content = new StackPanel { Orientation = Orientation.Horizontal }; 24 | if (GetIcon(action.Icon) is { } icon) 25 | { 26 | content.Children.Add(icon); 27 | } 28 | 29 | content.Children.Add(new PackIcon { Kind = PackIconKind.MenuDown }); 30 | button.Content = content; 31 | 32 | var contextMenu = new ContextMenu(); 33 | foreach (var child in action.Children) 34 | { 35 | contextMenu.Items.Add(child.ToMenuItem()); 36 | } 37 | 38 | button.ContextMenu = contextMenu; 39 | button.Click += (_, _) => button.OpenContextMenu(); 40 | } 41 | else 42 | { 43 | button.Content = GetIcon(action.Icon); 44 | button.Click += async (_, _) => await action.ExecuteAsync().ConfigureAwait(false); 45 | } 46 | 47 | return button; 48 | } 49 | 50 | public static MenuItem ToMenuItem(this IAction action) 51 | { 52 | var menuItem = new MenuItem 53 | { 54 | Header = action.DisplayName, 55 | ToolTip = action.Tooltip, 56 | Icon = GetIcon(action.Icon), 57 | InputGestureText = action.InputGestureText, 58 | }; 59 | menuItem.Click += async (_, _) => await action.ExecuteAsync().ConfigureAwait(false); 60 | foreach (var child in action.Children) 61 | { 62 | menuItem.Items.Add(child.ToMenuItem()); 63 | } 64 | 65 | return menuItem; 66 | } 67 | 68 | private static UIElement? GetIcon(object? icon) 69 | { 70 | if (icon is string str) 71 | { 72 | if (Enum.TryParse(str, out var packIconKind)) 73 | { 74 | return new PackIcon { Kind = packIconKind }; 75 | } 76 | 77 | return new Image { Source = new BitmapImage(new Uri(str)), Width = 20, Height = 20 }; 78 | } 79 | 80 | return null; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/Helpers/FrameworkElementExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.Windows; 5 | using System.Windows.Controls.Primitives; 6 | 7 | namespace GitTimelapseView.Wpf.Helpers 8 | { 9 | public static class FrameworkElementExtensions 10 | { 11 | public static void OpenContextMenu(this FrameworkElement frameworkElement) 12 | { 13 | var contextMenu = frameworkElement.ContextMenu; 14 | if (contextMenu == null) 15 | return; 16 | 17 | contextMenu.PlacementTarget = frameworkElement; 18 | contextMenu.Placement = PlacementMode.Bottom; 19 | 20 | contextMenu.IsOpen = true; 21 | if (contextMenu.Items.Count == 0) 22 | { 23 | contextMenu.IsOpen = false; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 32 | 33 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 67 | 70 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ubisoft. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0. 3 | 4 | using System.IO; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Input; 8 | using System.Windows.Media; 9 | using GitTimelapseView.Actions; 10 | using GitTimelapseView.Extensions; 11 | using GitTimelapseView.Helpers; 12 | using GitTimelapseView.Services; 13 | using GitTimelapseView.Wpf.Helpers; 14 | using Microsoft.AspNetCore.Components.Web; 15 | using Microsoft.AspNetCore.Components.WebView.Wpf; 16 | using Microsoft.Extensions.DependencyInjection; 17 | 18 | namespace GitTimelapseView 19 | { 20 | /// 21 | /// Interaction logic for MainWindow.xaml 22 | /// 23 | public partial class MainWindow 24 | { 25 | private readonly TimelapseService? _timelapseService; 26 | private readonly ThemingService? _themingService; 27 | 28 | public MainWindow(TimelapseService? timelapseService = null, ThemingService? themingService = null) 29 | { 30 | var rootDir = Path.GetDirectoryName(typeof(MainWindow).Assembly.Location); 31 | HostPage = rootDir != null ? Path.Combine(rootDir, @"wwwroot\index.html") : @"wwwroot\index.html"; 32 | Loaded += MainWindow_Loaded; 33 | InitializeComponent(); 34 | _timelapseService = timelapseService; 35 | _themingService = themingService; 36 | if (timelapseService != null) 37 | { 38 | timelapseService.FileLoading += (_, _) => UpdateTitle(); 39 | timelapseService.CurrentFileRevisionIndexChanged += (_, _) => UpdateTitle(); 40 | } 41 | 42 | UpdateTitle(); 43 | SourceInitialized += MainWindow_SourceInitialized; 44 | Closing += MainWindow_Closing; 45 | Reload(); 46 | } 47 | 48 | public string HostPage { get; } 49 | 50 | public void ExitApplication() 51 | { 52 | Close(); 53 | Application.Current.Shutdown(); 54 | } 55 | 56 | internal void Reload() 57 | { 58 | var webView = new BlazorWebView 59 | { 60 | HostPage = HostPage, 61 | Services = App.Current.ServiceProvider, 62 | Background = Brushes.Red, 63 | }; 64 | webView.RootComponents.Add(new RootComponent() { Selector = "head::after", ComponentType = typeof(HeadOutlet) }); 65 | 66 | var rootComponent = new RootComponent 67 | { 68 | Selector = "#app", 69 | ComponentType = typeof(RazorApp), 70 | }; 71 | webView.RootComponents.Add(rootComponent); 72 | _webViewGrid.Children.Clear(); 73 | _webViewGrid.Children.Add(webView); 74 | } 75 | 76 | private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) 77 | { 78 | if (WindowState == WindowState.Maximized) 79 | { 80 | Properties.Settings.Default.Top = RestoreBounds.Top; 81 | Properties.Settings.Default.Left = RestoreBounds.Left; 82 | Properties.Settings.Default.Height = RestoreBounds.Height; 83 | Properties.Settings.Default.Width = RestoreBounds.Width; 84 | Properties.Settings.Default.Maximized = true; 85 | } 86 | else 87 | { 88 | Properties.Settings.Default.Top = Top; 89 | Properties.Settings.Default.Left = Left; 90 | Properties.Settings.Default.Height = Height; 91 | Properties.Settings.Default.Width = Width; 92 | Properties.Settings.Default.Maximized = false; 93 | } 94 | 95 | Properties.Settings.Default.Save(); 96 | } 97 | 98 | private void MainWindow_SourceInitialized(object? sender, System.EventArgs e) 99 | { 100 | if (Properties.Settings.Default.Height > 0 && Properties.Settings.Default.Width > 0) 101 | { 102 | Top = Properties.Settings.Default.Top; 103 | Left = Properties.Settings.Default.Left; 104 | Height = Properties.Settings.Default.Height; 105 | Width = Properties.Settings.Default.Width; 106 | if (Properties.Settings.Default.Maximized) 107 | { 108 | WindowState = WindowState.Maximized; 109 | } 110 | } 111 | } 112 | 113 | private void MainWindow_Loaded(object sender, RoutedEventArgs e) 114 | { 115 | UpdateWindowButtonStates(); 116 | StateChanged += (_, _) => UpdateWindowButtonStates(); 117 | InitializeActions(); 118 | } 119 | 120 | private void InitializeActions() 121 | { 122 | var providers = App.Current.ServiceProvider.GetServices(); 123 | foreach (var provider in providers) 124 | { 125 | var commands = provider.GetActions(); 126 | foreach (var command in commands) 127 | { 128 | var button = command.ToButton(); 129 | button.Style = (Style)FindResource("TitleBarButtonStyle"); 130 | AdditionalCommandsItemsControl.Items.Add(button); 131 | } 132 | } 133 | 134 | FileMenu.Items.Add(new OpenFileAction().ToMenuItem()); 135 | FileMenu.Items.Add(new Separator()); 136 | FileMenu.Items.Add(new ExitApplicationAction().ToMenuItem()); 137 | HelpMenu.Items.Add(new ViewLogsAction().ToMenuItem()); 138 | HelpMenu.Items.Add(new AboutAction().ToMenuItem()); 139 | ViewMenu.Items.Add(CreateAppearanceMenu()); 140 | } 141 | 142 | private void OpenCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) 143 | { 144 | _ = new OpenFileAction().ExecuteAsync().ConfigureAwait(false); 145 | } 146 | 147 | private void CloseButton_Executed(object sender, ExecutedRoutedEventArgs e) => ExitApplication(); 148 | 149 | private void CloseButton_OnClick(object sender, RoutedEventArgs e) => ExitApplication(); 150 | 151 | private void MinimizeButton_OnClick(object sender, RoutedEventArgs e) 152 | { 153 | WindowState = WindowState.Minimized; 154 | } 155 | 156 | private void RestoreButton_OnClick(object sender, RoutedEventArgs e) 157 | { 158 | WindowState = WindowState.Normal; 159 | } 160 | 161 | private void MaximizeButton_OnClick(object sender, RoutedEventArgs e) 162 | { 163 | WindowState = WindowState.Maximized; 164 | } 165 | 166 | private void UpdateWindowButtonStates() 167 | { 168 | MaximizeButton.Visibility = WindowState == WindowState.Maximized ? Visibility.Collapsed : Visibility.Visible; 169 | RestoreButton.Visibility = WindowState == WindowState.Normal ? Visibility.Collapsed : Visibility.Visible; 170 | } 171 | 172 | private void UpdateTitle() 173 | { 174 | string title; 175 | if (_timelapseService != null && !string.IsNullOrEmpty(_timelapseService.FilePath)) 176 | { 177 | title = $"{Path.GetFileName(_timelapseService.FilePath)}#{_timelapseService.CurrentFileRevision?.Label} - {App.Current.ApplicationName}"; 178 | } 179 | else 180 | { 181 | title = nameof(GitTimelapseView); 182 | } 183 | 184 | Dispatcher.Invoke(() => Title = title); 185 | } 186 | 187 | private MenuItem CreateAppearanceMenu() 188 | { 189 | var appearanceMenuItem = new MenuItem 190 | { 191 | Header = "Appearance", 192 | }; 193 | if (_themingService != null) 194 | { 195 | foreach (var theme in _themingService.Themes) 196 | { 197 | var themeMenuItem = new MenuItem 198 | { 199 | Header = theme.Name, 200 | IsCheckable = true, 201 | IsChecked = _themingService.Theme == theme, 202 | Tag = theme, 203 | }; 204 | themeMenuItem.Click += (_, _) => 205 | { 206 | _themingService.ApplyTheme(theme); 207 | UpdateCheckedState(appearanceMenuItem); 208 | }; 209 | appearanceMenuItem.Items.Add(themeMenuItem); 210 | } 211 | } 212 | 213 | return appearanceMenuItem; 214 | 215 | void UpdateCheckedState(MenuItem menuItem) 216 | { 217 | foreach (var item in menuItem.Items.OfType()) 218 | { 219 | item.IsChecked = item.Tag is ThemeInfo themeInfo && _themingService.Theme == themeInfo; 220 | } 221 | } 222 | } 223 | } 224 | 225 | #pragma warning disable MA0048,SA1601,SA1402 226 | public partial class RazorApp 227 | { 228 | } 229 | #pragma warning restore SA1402,SA1601,MA0048 230 | } 231 | -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/Resources/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/Wpf/Resources/appicon.ico -------------------------------------------------------------------------------- /GitTimelapseView/Wpf/Resources/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/Wpf/Resources/appicon.png -------------------------------------------------------------------------------- /GitTimelapseView/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using System.Linq 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorMonaco 10 | @using GitTimelapseView 11 | @using GitTimelapseView.Actions 12 | @using GitTimelapseView.Data 13 | @using GitTimelapseView.Extensions 14 | @using GitTimelapseView.Helpers 15 | @using GitTimelapseView.Core.Models 16 | @using GitTimelapseView.Services 17 | @using AntDesign 18 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .ant-card-bordered { 2 | border-color: var(--gtlv-card-border); 3 | } 4 | 5 | .ant-card-body { 6 | padding: 0px; 7 | overflow: scroll; 8 | } 9 | 10 | .ant-card-head { 11 | background: var(--gtlv-card-header-background); 12 | } 13 | 14 | .ant-table-small .ant-table-thead > tr > th { 15 | background: transparent; 16 | } 17 | 18 | .ant-table.ant-table-small .ant-table-footer, .ant-table.ant-table-small .ant-table-tbody > tr > td, .ant-table.ant-table-small .ant-table-thead > tr > th, .ant-table.ant-table-small .ant-table-title, .ant-table.ant-table-small tfoot > tr > td, .ant-table.ant-table-small tfoot > tr > th { 19 | padding: 4px; 20 | } 21 | 22 | .ant-layout-sider { 23 | background: transparent; 24 | min-width:0px; 25 | width: auto; 26 | } 27 | 28 | .ant-tabs-large > .ant-tabs-nav .ant-tabs-tab { 29 | padding: 8px 0px; 30 | } 31 | 32 | #root-container { 33 | background: var(--gtlv-background); 34 | color: var(--gtlv-foreground); 35 | } 36 | 37 | ::-webkit-scrollbar { 38 | width: 16px; 39 | height: 0px; 40 | } 41 | 42 | ::-webkit-scrollbar-button { 43 | visibility: collapse; 44 | height: 0px; 45 | } 46 | 47 | ::-webkit-scrollbar-track { 48 | background-color: transparent; 49 | } 50 | 51 | ::-webkit-scrollbar-track-piece { 52 | background-color: var(--gtlv-scrollbar-background); 53 | } 54 | 55 | ::-webkit-scrollbar-thumb { 56 | height: 50px; 57 | background-color: var(--gtlv-scrollbar-thumb); 58 | } 59 | 60 | ::-webkit-scrollbar-thumb:hover { 61 | background-color: var(--gtlv-scrollbar-thumb-hover); 62 | } 63 | 64 | ::-webkit-scrollbar-corner { 65 | visibility: collapse; 66 | height: 0px; 67 | } 68 | 69 | ::-webkit-resizer { 70 | visibility: collapse; 71 | height: 0px; 72 | } 73 | 74 | .far { 75 | color: var(--gtlv-foreground); 76 | } 77 | 78 | .tooltip-inner { 79 | text-align:left; 80 | } 81 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/css/theme.dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gtlv-background: #333333; 3 | --gtlv-foreground: #EEE; 4 | --gtlv-card-background: #1E1E1E; 5 | --gtlv-card-header-background: #191919; 6 | --gtlv-card-border: #1D1D1D; 7 | --gtlv-scrollbar-background: #1E1E1E; 8 | --gtlv-scrollbar-thumb: #424242; 9 | --gtlv-scrollbar-thumb-hover: #4F4F4F; 10 | --gtlv-table-background: #1E1E1E; 11 | --gtlv-table-hover: #333333; 12 | --gtlv-tab-background: #1F1F1F; 13 | --gtlv-tab-foreground: #EEE; 14 | --gtlv-tab-border: #444444; 15 | --gtlv-jumbotron-background: #191919; 16 | --gtlv-editor-highlight: rgba(255, 165, 0, 0.15); 17 | --gtlv-editor-margin: #397182; 18 | --gtlv-editor-margin-alt: #574A77; 19 | --gtlv-page-progress-rail: #333333; 20 | --gtlv-page-progress-track: #16436E; 21 | } 22 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/css/theme.light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gtlv-background: #FFFFFF; 3 | --gtlv-foreground: #000; 4 | --gtlv-card-background: #FFFFFF; 5 | --gtlv-card-header-background: #F7F7F7; 6 | --gtlv-card-border: #E0E0E0; 7 | --gtlv-scrollbar-background: #F8F9FA; 8 | --gtlv-scrollbar-thumb: #C1C1C0; 9 | --gtlv-scrollbar-thumb-hover: #929292; 10 | --gtlv-table-background: #FFFFFF; 11 | --gtlv-table-hover: #ECECEC; 12 | --gtlv-tab-background: #FFFFFF; 13 | --gtlv-tab-foreground: #495057; 14 | --gtlv-tab-border: #DEE2E6; 15 | --gtlv-jumbotron-background: #F7F7F7; 16 | --gtlv-editor-highlight: rgba(255, 165, 0, 0.15); 17 | --gtlv-editor-margin: lightblue; 18 | --gtlv-editor-margin-alt: #9084AE; 19 | --gtlv-page-progress-rail: #F5F5F5; 20 | --gtlv-page-progress-track: #16436E; 21 | } 22 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/extern/mdi@6.5.95/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/wwwroot/extern/mdi@6.5.95/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/extern/threedots/threedots.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! 2 | * three-dots - v0.2.1 3 | * CSS loading animation made by single element. 4 | * https://nzbin.github.io/three-dots/ 5 | * 6 | * Copyright (c) 2018 nzbin 7 | * Released under MIT License 8 | */.dot-pulse{position:relative;left:-9999px;width:10px;height:10px;border-radius:5px;background-color:white;color:white;box-shadow:9999px 0 0 -5px;-webkit-animation:dot-pulse 1.5s infinite linear;animation:dot-pulse 1.5s infinite linear;-webkit-animation-delay:.25s;animation-delay:.25s}.dot-pulse::after,.dot-pulse::before{content:"";display:inline-block;position:absolute;top:0;width:10px;height:10px;border-radius:5px;background-color:white;color:white}.dot-pulse::before{box-shadow:9984px 0 0 -5px;-webkit-animation:dot-pulse-before 1.5s infinite linear;animation:dot-pulse-before 1.5s infinite linear;-webkit-animation-delay:0s;animation-delay:0s}.dot-pulse::after{box-shadow:10014px 0 0 -5px;-webkit-animation:dot-pulse-after 1.5s infinite linear;animation:dot-pulse-after 1.5s infinite linear;-webkit-animation-delay:.5s;animation-delay:.5s}@-webkit-keyframes dot-pulse-before{0%{box-shadow:9984px 0 0 -5px}30%{box-shadow:9984px 0 0 2px}100%,60%{box-shadow:9984px 0 0 -5px}}@keyframes dot-pulse-before{0%{box-shadow:9984px 0 0 -5px}30%{box-shadow:9984px 0 0 2px}100%,60%{box-shadow:9984px 0 0 -5px}}@-webkit-keyframes dot-pulse{0%{box-shadow:9999px 0 0 -5px}30%{box-shadow:9999px 0 0 2px}100%,60%{box-shadow:9999px 0 0 -5px}}@keyframes dot-pulse{0%{box-shadow:9999px 0 0 -5px}30%{box-shadow:9999px 0 0 2px}100%,60%{box-shadow:9999px 0 0 -5px}}@-webkit-keyframes dot-pulse-after{0%{box-shadow:10014px 0 0 -5px}30%{box-shadow:10014px 0 0 2px}100%,60%{box-shadow:10014px 0 0 -5px}}@keyframes dot-pulse-after{0%{box-shadow:10014px 0 0 -5px}30%{box-shadow:10014px 0 0 2px}100%,60%{box-shadow:10014px 0 0 -5px}}.dot-flashing{position:relative;width:10px;height:10px;border-radius:5px;background-color:white;color:white;-webkit-animation:dot-flashing 1s infinite linear alternate;animation:dot-flashing 1s infinite linear alternate;-webkit-animation-delay:.5s;animation-delay:.5s}.dot-flashing::after,.dot-flashing::before{content:'';display:inline-block;position:absolute;top:0}.dot-flashing::before{left:-15px;width:10px;height:10px;border-radius:5px;background-color:white;color:white;-webkit-animation:dot-flashing 1s infinite alternate;animation:dot-flashing 1s infinite alternate;-webkit-animation-delay:0s;animation-delay:0s}.dot-flashing::after{left:15px;width:10px;height:10px;border-radius:5px;background-color:white;color:white;-webkit-animation:dot-flashing 1s infinite alternate;animation:dot-flashing 1s infinite alternate;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes dot-flashing{0%{background-color:white}100%,50%{background-color:#ebe6ff}}@keyframes dot-flashing{0%{background-color:white}100%,50%{background-color:#ebe6ff}} 9 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GitTimelapseView 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/scripts/GeneralScripts.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/GitTimelapseView/wwwroot/scripts/GeneralScripts.js -------------------------------------------------------------------------------- /GitTimelapseView/wwwroot/scripts/TextEditorScripts.js: -------------------------------------------------------------------------------- 1 | function setScroll(query, scrollTop, scrollHeight) { 2 | element = $(query)[0]; 3 | element.scrollTop = scrollTop; 4 | element.scrollHeight = scrollHeight; 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | ### 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | ### 2. Grant of Copyright License. 32 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 33 | 34 | ### 3. Grant of Patent License. 35 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 36 | 37 | ### 4. Redistribution. 38 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 39 | 40 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 41 | You must cause any modified files to carry prominent notices stating that You changed the files; and 42 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 43 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 44 | 45 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 46 | 47 | ### 5. Submission of Contributions. 48 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 49 | 50 | ### 6. Trademarks. 51 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 52 | 53 | ### 7. Disclaimer of Warranty. 54 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 55 | 56 | ### 8. Limitation of Liability. 57 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 58 | 59 | ### 9. Accepting Warranty or Additional Liability. 60 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitTimelapseView 2 | 3 | GitTimelapseView allows you to easily monitor the evolution of your Git files over time. It adds a timeline to your blame view 4 | 5 | ![GitTimelapseView screenshot](docs/img/screenshot01.png) 6 | 7 | ## Why is it helpful? 8 | 9 | Viewing the different versions of a file on the same window will help you find the exact time and commit which introduced new bugs or features. By using the git project's history GitTimelapseView also helps you find the last person who modified each part of the file so you always know who to ask when the code gets obscure. 10 | 11 | ## Contributing 12 | 13 | We would love you to contribute to `@ubisoft/GitTimeLapseView`, pull requests are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for more information. 14 | 15 | ## License 16 | 17 | The scripts and documentation in this project are released under the [Apache License](LICENSE) 18 | -------------------------------------------------------------------------------- /docs/img/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/docs/img/screenshot01.png -------------------------------------------------------------------------------- /docs/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubisoft/GitTimeLapseView/cfa5314c9b171e8443a8f1f8c406206c54632193/docs/screenshot01.png --------------------------------------------------------------------------------