зеркало из https://github.com/github/VisualStudio.git
Implement VSPackage with basic login dialog
Includes a menu that launches a login dialog for testing purposes.
This commit is contained in:
Родитель
2f00f72032
Коммит
7034a030ac
|
@ -0,0 +1,6 @@
|
|||
[submodule "Rothko"]
|
||||
path = Rothko
|
||||
url = https://github.com/Haacked/Rothko.git
|
||||
[submodule "submodules/rothko"]
|
||||
path = submodules/rothko
|
||||
url = https://github.com/Haacked/Rothko.git
|
79
GitHubVS.sln
79
GitHubVS.sln
|
@ -3,39 +3,90 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||
# Visual Studio 2013
|
||||
VisualStudioVersion = 12.0.31101.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{11569514-5AE5-4B5B-92A2-F10B0967DE5F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6A6B0910-ECED-4FD6-8563-359FFDC23373}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
build\CodeAnalysisDictionary.xml = build\CodeAnalysisDictionary.xml
|
||||
build\GitHubVS.ruleset = build\GitHubVS.ruleset
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{880CC82E-4419-45F7-8C32-4687D0E1A175}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{72036B62-2FA6-4A22-8B33-69F698A18CF1}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{F8947717-5582-48D1-BDFA-3CFD5BC3C31B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
build\Bootstrap.ps1 = build\Bootstrap.ps1
|
||||
build.cmd = build.cmd
|
||||
build\cibuild.ps1 = build\cibuild.ps1
|
||||
build\SolutionInfo.cs = build\SolutionInfo.cs
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "src\UnitTests\UnitTests.csproj", "{596595A6-2A3C-469E-9386-9E3767D863A5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\GitHub.UI\GitHub.UI.csproj", "{346384DD-2445-4A28-AF22-B45F3957BD89}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Reactive", "src\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj", "{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Extensions", "src\GitHub.Extensions\GitHub.Extensions.csproj", "{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Extensions.Reactive", "src\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj", "{6559E128-8B40-49A5-85A8-05565ED0C7E3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.App", "src\GitHub.App\GitHub.App.csproj", "{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Submodules", "Submodules", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rothko", "submodules\Rothko\src\Rothko.csproj", "{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{8E1F1B4E-AEA2-4AB1-8F73-423A903550A1}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
build\Modules\BuildUtils.psm1 = build\Modules\BuildUtils.psm1
|
||||
build\Modules\Debugging.psm1 = build\Modules\Debugging.psm1
|
||||
build\Modules\Vsix.psm1 = build\Modules\Vsix.psm1
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}
|
||||
{8E1F1B4E-AEA2-4AB1-8F73-423A903550A1} = {F8947717-5582-48D1-BDFA-3CFD5BC3C31B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
powershell.exe .\build\cibuild.ps1
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><test-results name="C:\dev\VisualStudio\src\UnitTests\bin\Release\UnitTests.dll" date="2014-12-15" time="15:43:47" total="4" failures="0" not-run="0"><test-suite name="C:\dev\VisualStudio\src\UnitTests\bin\Release\UnitTests.dll" success="True" time="0.697"><results><test-suite name="TodoTaggerTests+TheGetTagsMethod" success="True" time="0.697"><results><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: "// TODO: foo bar")" executed="True" success="True" time="0.683" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: "//TODO foo bar")" executed="True" success="True" time="0.003" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: "// TODO foo bar")" executed="True" success="True" time="0.003" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: "//TODO: foo bar")" executed="True" success="True" time="0.008" /></results></test-suite></results></test-suite></test-results>
|
|
@ -0,0 +1,22 @@
|
|||
namespace GitHub.Authentication
|
||||
{
|
||||
public enum AuthenticationResult
|
||||
{
|
||||
CredentialFailure,
|
||||
VerificationFailure,
|
||||
Success
|
||||
}
|
||||
|
||||
public static class AuthenticationResultExtensions
|
||||
{
|
||||
public static bool IsFailure(this AuthenticationResult result)
|
||||
{
|
||||
return result != AuthenticationResult.Success;
|
||||
}
|
||||
|
||||
public static bool IsSuccess(this AuthenticationResult result)
|
||||
{
|
||||
return result == AuthenticationResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using GitHub.Models;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IAccount : IReactiveObject
|
||||
{
|
||||
string Email { get; }
|
||||
int Id { get; }
|
||||
bool IsEnterprise { get; }
|
||||
bool IsGitHub { get; }
|
||||
bool IsLocal { get; }
|
||||
bool IsOnFreePlan { get; }
|
||||
bool HasMaximumPrivateRepositories { get; }
|
||||
bool IsUser { get; }
|
||||
/// <summary>
|
||||
/// True if the user is an admin on the host (GitHub or Enterprise).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not confuse this with "IsStaff". This is true if the user is an admin
|
||||
/// on the site. IsStaff is true if that site is github.com.
|
||||
/// </remarks>
|
||||
bool IsSiteAdmin { get; }
|
||||
/// <summary>
|
||||
/// Returns true if the user is a member of the GitHub staff.
|
||||
/// </summary>
|
||||
bool IsGitHubStaff { get; }
|
||||
IRepositoryHost Host { get; }
|
||||
string Login { get; }
|
||||
string Name { get; }
|
||||
int OwnedPrivateRepos { get; }
|
||||
long PrivateReposInPlan { get; }
|
||||
|
||||
void Update(User ghUser);
|
||||
void Update(Organization org);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub.Authentication
|
||||
{
|
||||
public interface ITwoFactorChallengeHandler
|
||||
{
|
||||
IObservable<TwoFactorChallengeResult> HandleTwoFactorException(TwoFactorRequiredException exception);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using GitHub.ViewModels;
|
||||
using Microsoft.VisualStudio.PlatformUI;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Authentication
|
||||
{
|
||||
[Export(typeof(ITwoFactorChallengeHandler))]
|
||||
public class TwoFactorChallengeHandler : ITwoFactorChallengeHandler
|
||||
{
|
||||
readonly IServiceProvider serviceProvider;
|
||||
|
||||
[ImportingConstructor]
|
||||
public TwoFactorChallengeHandler(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IObservable<TwoFactorChallengeResult> HandleTwoFactorException(TwoFactorRequiredException exception)
|
||||
{
|
||||
var twoFactorDialog = (TwoFactorDialogViewModel)serviceProvider.GetService(typeof(TwoFactorDialogViewModel));
|
||||
var twoFactorView = (IViewFor<TwoFactorDialogViewModel>)serviceProvider.GetService(typeof(IViewFor<TwoFactorDialogViewModel>));
|
||||
|
||||
return Observable.Start(() =>
|
||||
{
|
||||
twoFactorView.ViewModel = twoFactorDialog;
|
||||
((DialogWindow)twoFactorView).Show();
|
||||
|
||||
var userError = new TwoFactorRequiredUserError(exception);
|
||||
return twoFactorDialog.Show(userError)
|
||||
.SelectMany(x =>
|
||||
x == RecoveryOptionResult.RetryOperation
|
||||
? Observable.Return(userError.ChallengeResult)
|
||||
: Observable.Throw<TwoFactorChallengeResult>(exception));
|
||||
}, RxApp.MainThreadScheduler)
|
||||
.SelectMany(x => x)
|
||||
.Finally(() =>
|
||||
((DialogWindow)twoFactorView).Hide());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using NullGuard;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Authentication
|
||||
{
|
||||
public class TwoFactorRequiredUserError : UserError
|
||||
{
|
||||
public TwoFactorRequiredUserError(TwoFactorRequiredException exception)
|
||||
: base(exception.Message, innerException: exception)
|
||||
{
|
||||
TwoFactorType = exception.TwoFactorType;
|
||||
}
|
||||
|
||||
public TwoFactorType TwoFactorType { get; private set; }
|
||||
|
||||
[AllowNull]
|
||||
public TwoFactorChallengeResult ChallengeResult
|
||||
{
|
||||
[return: AllowNull]
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IObservable<RecoveryOptionResult> Throw()
|
||||
{
|
||||
return Throw(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Akavache;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Extensions.Reactive;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public class HostCache : IHostCache
|
||||
{
|
||||
readonly IBlobCache localMachineCache;
|
||||
readonly IBlobCache userAccountCache;
|
||||
|
||||
public HostCache(IBlobCache localMachineCache, IBlobCache userAccountCache)
|
||||
{
|
||||
this.localMachineCache = localMachineCache;
|
||||
this.userAccountCache = userAccountCache;
|
||||
}
|
||||
|
||||
public IObservable<User> GetUser()
|
||||
{
|
||||
return userAccountCache.GetObject<User>("user");
|
||||
}
|
||||
|
||||
public IObservable<Unit> InsertUser(User user)
|
||||
{
|
||||
return userAccountCache.InsertObject("user", user);
|
||||
}
|
||||
|
||||
public IObservable<IEnumerable<Organization>> GetAllOrganizations()
|
||||
{
|
||||
// NOTE: Akavache with the SQLite storage stores the type name with the cached objects
|
||||
// to support the GetAllObjects method call. This is the only usage of that method
|
||||
// which is why we have to keep GitHubOrganization around.
|
||||
return userAccountCache.GetAllObjects<Organization>();
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
|
||||
Justification = "We store the user differently")]
|
||||
public IObservable<Unit> InsertOrganization(Organization organization)
|
||||
{
|
||||
return userAccountCache.InsertObject(organization.Login, organization);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateOrganization(Organization organization)
|
||||
{
|
||||
return InvalidateOrganization(organization.Login);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateOrganization(IAccount organization)
|
||||
{
|
||||
return InvalidateOrganization(organization.Login);
|
||||
}
|
||||
|
||||
IObservable<Unit> InvalidateOrganization(string login)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(login, "login");
|
||||
|
||||
return userAccountCache.InvalidateObject<Organization>(login);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateAll()
|
||||
{
|
||||
return Observable.Merge
|
||||
(
|
||||
localMachineCache.InvalidateAll(),
|
||||
userAccountCache.InvalidateAll(),
|
||||
Invalidate()
|
||||
).AsCompletion();
|
||||
}
|
||||
|
||||
protected virtual IObservable<Unit> Invalidate()
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
localMachineCache.Dispose();
|
||||
localMachineCache.Shutdown.Wait();
|
||||
userAccountCache.Dispose();
|
||||
userAccountCache.Shutdown.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using Rothko;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(IHostCacheFactory))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class HostCacheFactory : IHostCacheFactory
|
||||
{
|
||||
readonly Lazy<IBlobCacheFactory> blobCacheFactory;
|
||||
readonly Lazy<IOperatingSystemFacade> operatingSystem;
|
||||
|
||||
[ImportingConstructor]
|
||||
public HostCacheFactory(Lazy<IBlobCacheFactory> blobCacheFactory, Lazy<IOperatingSystemFacade> operatingSystem)
|
||||
{
|
||||
this.blobCacheFactory = blobCacheFactory;
|
||||
this.operatingSystem = operatingSystem;
|
||||
}
|
||||
|
||||
public IHostCache Create(HostAddress hostAddress)
|
||||
{
|
||||
var environment = OperatingSystem.Environment;
|
||||
// For GitHub.com, the cache file name should be "api.github.com.cache.db"
|
||||
// This is why we use ApiUrl and not CredentialCacheHostKey
|
||||
string host = hostAddress.ApiUri.Host;
|
||||
string cacheFileName = host + ".cache.db";
|
||||
|
||||
var localMachinePath = Path.Combine(environment.GetLocalGitHubApplicationDataPath(), cacheFileName);
|
||||
var userAccountPath = Path.Combine(environment.GetApplicationDataPath(), cacheFileName);
|
||||
|
||||
// CreateDirectory is a noop if the directory already exists.
|
||||
new[] { localMachinePath, userAccountPath }
|
||||
.ForEach(x => OperatingSystem.Directory.CreateDirectory(Path.GetDirectoryName(x)));
|
||||
|
||||
var localMachineCache = BlobCacheFactory.CreateBlobCache(localMachinePath);
|
||||
var userAccountCache = BlobCacheFactory.CreateBlobCache(userAccountPath);
|
||||
|
||||
return new HostCache(localMachineCache, userAccountCache);
|
||||
}
|
||||
|
||||
IOperatingSystemFacade OperatingSystem { get { return operatingSystem.Value; } }
|
||||
IBlobCacheFactory BlobCacheFactory { get { return blobCacheFactory.Value; } }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using Akavache;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IBlobCacheFactory
|
||||
{
|
||||
IBlobCache CreateBlobCache(string path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Per host cache data.
|
||||
/// </summary>
|
||||
public interface IHostCache : IDisposable
|
||||
{
|
||||
IObservable<User> GetUser();
|
||||
IObservable<Unit> InsertUser(User user);
|
||||
IObservable<IEnumerable<Organization>> GetAllOrganizations();
|
||||
IObservable<Unit> InsertOrganization(Organization organization);
|
||||
IObservable<Unit> InvalidateOrganization(Organization organization);
|
||||
IObservable<Unit> InvalidateOrganization(IAccount organization);
|
||||
IObservable<Unit> InvalidateAll();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IHostCacheFactory
|
||||
{
|
||||
IHostCache Create(HostAddress hostAddress);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Reactive;
|
||||
using Akavache;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface ILoginCache : IDisposable
|
||||
{
|
||||
IObservable<LoginInfo> GetLoginAsync(HostAddress hostAddress);
|
||||
IObservable<Unit> SaveLogin(string user, string password, HostAddress hostAddress);
|
||||
IObservable<Unit> EraseLogin(HostAddress hostAddress);
|
||||
IObservable<Unit> Flush();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reactive;
|
||||
using Akavache;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
/// <summary>
|
||||
/// A cache for data that's not host specific
|
||||
/// </summary>
|
||||
public interface ISharedCache : IDisposable
|
||||
{
|
||||
IBlobCache UserAccount { get; }
|
||||
IBlobCache LocalMachine { get; }
|
||||
ISecureBlobCache Secure { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Enterpise Host Uri from cache if present.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IObservable<Uri> GetEnterpriseHostUri();
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the Enterprise Host Uri.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IObservable<Unit> InsertEnterpriseHostUri(Uri enterpriseHostUri);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Enterprise Host Uri from the cache.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IObservable<Unit> InvalidateEnterpriseHostUri();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves whether staff mode is enabled.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IObservable<bool> GetStaffMode();
|
||||
|
||||
/// <summary>
|
||||
/// Helper method used to bind a property of a view model to a value in the cache.
|
||||
/// </summary>
|
||||
IDisposable BindPropertyToCache<TSource, TProperty>(TSource source,
|
||||
Expression<Func<TSource, TProperty>> property,
|
||||
TProperty defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// Helper method used to bind a property of a view model to a value in the cache with a mapping function to
|
||||
/// and from the cache.
|
||||
/// </summary>
|
||||
IDisposable BindPropertyToCache<TSource, TProperty, TCacheValue>(TSource source,
|
||||
Expression<Func<TSource, TProperty>> property,
|
||||
Func<TProperty, TCacheValue> mapToCache,
|
||||
Func<TCacheValue, TProperty> mapFromCache,
|
||||
TCacheValue defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether staff mode is enabled;
|
||||
/// </summary>
|
||||
/// <param name="staffMode"></param>
|
||||
/// <returns></returns>
|
||||
IObservable<Unit> SetStaffMode(bool staffMode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Globalization;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Akavache;
|
||||
using GitHub.Extensions;
|
||||
using NLog;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(ILoginCache))]
|
||||
public sealed class LoginCache : ILoginCache
|
||||
{
|
||||
static readonly Logger log = LogManager.GetCurrentClassLogger();
|
||||
readonly ISharedCache cache;
|
||||
|
||||
static readonly LoginInfo empty = new LoginInfo("", "");
|
||||
|
||||
[ImportingConstructor]
|
||||
public LoginCache(ISharedCache cache)
|
||||
{
|
||||
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
public static LoginInfo EmptyLoginInfo
|
||||
{
|
||||
get { return empty; }
|
||||
}
|
||||
|
||||
public IObservable<LoginInfo> GetLoginAsync(HostAddress hostAddress)
|
||||
{
|
||||
return cache.Secure.GetLoginAsync(hostAddress.CredentialCacheKeyHost).Catch(Observable.Return(empty));
|
||||
}
|
||||
|
||||
public IObservable<Unit> SaveLogin(string user, string password, HostAddress hostAddress)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(user, "user");
|
||||
Guard.ArgumentNotEmptyString(password, "password");
|
||||
|
||||
return cache.Secure.SaveLogin(user, password, hostAddress.CredentialCacheKeyHost);
|
||||
}
|
||||
|
||||
public IObservable<Unit> EraseLogin(HostAddress hostAddress)
|
||||
{
|
||||
log.Info(CultureInfo.CurrentCulture, "Erasing the git credential cache for host '{0}'",
|
||||
hostAddress.CredentialCacheKeyHost);
|
||||
return cache.Secure.EraseLogin(hostAddress.CredentialCacheKeyHost);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Flush()
|
||||
{
|
||||
log.Info("Flushing the login cache");
|
||||
return cache.Secure.Flush();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using Akavache;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
/// <summary>
|
||||
/// A cache for data that's not host specific
|
||||
/// </summary>
|
||||
[Export(typeof(ISharedCache))]
|
||||
public class SharedCache : ISharedCache
|
||||
{
|
||||
const string enterpriseHostApiBaseUriCacheKey = "enterprise-host-api-base-uri";
|
||||
const string staffModeKey = "__StaffOnlySettings__:IsStaffMode";
|
||||
|
||||
// TODO Use this instead.
|
||||
//public SharedCache()
|
||||
// : this(BlobCache.UserAccount, BlobCache.LocalMachine, BlobCache.Secure)
|
||||
//{
|
||||
//}
|
||||
|
||||
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new InMemoryBlobCache())
|
||||
{
|
||||
}
|
||||
|
||||
protected SharedCache(IBlobCache userAccountCache, IBlobCache localMachineCache, ISecureBlobCache secureCache)
|
||||
{
|
||||
UserAccount = userAccountCache;
|
||||
LocalMachine = localMachineCache;
|
||||
Secure = secureCache;
|
||||
}
|
||||
|
||||
public IBlobCache UserAccount { get; private set; }
|
||||
public IBlobCache LocalMachine { get; private set; }
|
||||
public ISecureBlobCache Secure { get; private set; }
|
||||
|
||||
public IObservable<Uri> GetEnterpriseHostUri()
|
||||
{
|
||||
return UserAccount.GetObject<Uri>(enterpriseHostApiBaseUriCacheKey);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InsertEnterpriseHostUri(Uri enterpriseHostUri)
|
||||
{
|
||||
return UserAccount.InsertObject(enterpriseHostApiBaseUriCacheKey, enterpriseHostUri);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateEnterpriseHostUri()
|
||||
{
|
||||
return UserAccount.InvalidateObject<Uri>(enterpriseHostApiBaseUriCacheKey);
|
||||
}
|
||||
|
||||
public IObservable<bool> GetStaffMode()
|
||||
{
|
||||
return UserAccount.GetOrFetchObject(staffModeKey, () => Observable.Return(false));
|
||||
}
|
||||
|
||||
public IObservable<Unit> SetStaffMode(bool staffMode)
|
||||
{
|
||||
return UserAccount.InsertObject(staffModeKey, staffMode);
|
||||
}
|
||||
|
||||
public IDisposable BindPropertyToCache<TSource, TProperty>(TSource source,
|
||||
Expression<Func<TSource, TProperty>> property,
|
||||
TProperty defaultValue)
|
||||
{
|
||||
return BindPropertyToCache(source, property, x => x, x => x, defaultValue);
|
||||
}
|
||||
|
||||
public IDisposable BindPropertyToCache<TSource, TProperty, TCacheValue>(TSource source,
|
||||
Expression<Func<TSource, TProperty>> property,
|
||||
Func<TProperty, TCacheValue> mapToCache,
|
||||
Func<TCacheValue, TProperty> mapFromCache,
|
||||
TCacheValue defaultValue)
|
||||
{
|
||||
var propertyInfo = GetPropertyNameAndSetAction(property, source);
|
||||
var propertySetter = propertyInfo.Item2;
|
||||
propertySetter(mapFromCache(defaultValue));
|
||||
var restoreCachedValue = new Action<TCacheValue>(value => propertySetter(mapFromCache(value)));
|
||||
|
||||
// TODO: We may want a way to preserve the cache key should we ever rename the property or type.
|
||||
// We could look for specific attributes to do this. But for now, let's not worry about it.
|
||||
|
||||
string cacheKey = "Application:" + typeof(TSource).Name + ":" + propertyInfo.Item1;
|
||||
var propertyObservable = source.WhenAny(property, prop => prop.Value).Select(mapToCache);
|
||||
|
||||
return LocalMachine.GetObject<TCacheValue>(cacheKey)
|
||||
.Catch(Observable.Return(defaultValue))
|
||||
.SingleAsync()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(cachedValue =>
|
||||
{
|
||||
restoreCachedValue(cachedValue);
|
||||
propertyObservable
|
||||
.Throttle(TimeSpan.FromSeconds(2), RxApp.TaskpoolScheduler)
|
||||
.SelectMany(width => LocalMachine.InsertObject(cacheKey, width))
|
||||
.Subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
static Tuple<string, Action<TProperty>> GetPropertyNameAndSetAction<TSource, TProperty>(
|
||||
Expression<Func<TSource, TProperty>> expression,
|
||||
TSource source)
|
||||
{
|
||||
var member = expression.Body as MemberExpression;
|
||||
Debug.Assert(member != null, "Expression should be a property and not method or some other shit.");
|
||||
var property = member.Member as PropertyInfo;
|
||||
Debug.Assert(property != null, "Expression should be a property and not field or some other shit.");
|
||||
var propertySetterAction = new Action<TProperty>(value => property.SetValue(source, value));
|
||||
return Tuple.Create(property.Name, propertySetterAction);
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
UserAccount.Dispose();
|
||||
UserAccount.Shutdown.Wait();
|
||||
LocalMachine.Dispose();
|
||||
LocalMachine.Shutdown.Wait();
|
||||
Secure.Dispose();
|
||||
Secure.Shutdown.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using Akavache;
|
||||
using Akavache.Sqlite3;
|
||||
using GitHub.Extensions;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(IBlobCacheFactory))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class SqlitePersistentBlobCacheFactory : IBlobCacheFactory
|
||||
{
|
||||
public IBlobCache CreateBlobCache(string path)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(path, "path");
|
||||
|
||||
return new InMemoryBlobCache();
|
||||
//return new SQLitePersistentBlobCache(path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -0,0 +1,192 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>GitHub</RootNamespace>
|
||||
<AssemblyName>GitHub.App</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>ee7f8637</NuGetPackageImportStamp>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Akavache">
|
||||
<HintPath>..\..\packages\akavache.core.4.0.3.2\lib\net45\Akavache.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Akavache.Sqlite3">
|
||||
<HintPath>..\..\packages\akavache.sqlite3.4.0.3.2\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NLog">
|
||||
<HintPath>..\..\packages\NLog.3.1.0.0\lib\net45\NLog.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NullGuard">
|
||||
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Octokit">
|
||||
<HintPath>..\..\packages\Octokit.0.4.1\lib\net45\Octokit.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Octokit.Reactive">
|
||||
<HintPath>..\..\packages\Octokit.Reactive.0.4.1\lib\net45\Octokit.Reactive.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="ReactiveUI">
|
||||
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Reactive.Core">
|
||||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces">
|
||||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq">
|
||||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.PlatformServices">
|
||||
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Windows.Threading">
|
||||
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Authentication\AuthenticationResult.cs" />
|
||||
<Compile Include="Authentication\IAccount.cs" />
|
||||
<Compile Include="Authentication\ITwoFactorChallengeHandler.cs" />
|
||||
<Compile Include="Authentication\TwoFactorChallengeHandler.cs" />
|
||||
<Compile Include="Authentication\TwoFactorRequiredUserError.cs" />
|
||||
<Compile Include="Caches\HostCache.cs" />
|
||||
<Compile Include="Caches\HostCacheFactory.cs" />
|
||||
<Compile Include="Caches\IBlobCacheFactory.cs" />
|
||||
<Compile Include="Caches\IHostCache.cs" />
|
||||
<Compile Include="Caches\IHostCacheFactory.cs" />
|
||||
<Compile Include="Caches\ILoginCache.cs" />
|
||||
<Compile Include="Caches\ISharedCache.cs" />
|
||||
<Compile Include="Caches\LoginCache.cs" />
|
||||
<Compile Include="Caches\SharedCache.cs" />
|
||||
<Compile Include="Caches\SqlitePersistentBlobCacheFactory.cs" />
|
||||
<Compile Include="Info\EnvironmentExtensions.cs" />
|
||||
<Compile Include="Info\GitHubUrls.cs" />
|
||||
<Compile Include="Infrastructure\AppModeDetector.cs" />
|
||||
<Compile Include="Infrastructure\ExportWrappers.cs" />
|
||||
<Compile Include="Models\Account.cs" />
|
||||
<Compile Include="Models\DisconnectedRepositoryHosts.cs" />
|
||||
<Compile Include="Models\LocalRepositoriesAccount.cs" />
|
||||
<Compile Include="Models\LocalRepositoriesHost.cs" />
|
||||
<Compile Include="Models\Program.cs" />
|
||||
<Compile Include="Models\RepositoryHost.cs" />
|
||||
<Compile Include="Models\RepositoryHosts.cs" />
|
||||
<Compile Include="Primitives\HostAddress.cs" />
|
||||
<Compile Include="Models\IProgram.cs" />
|
||||
<Compile Include="Models\IRepositoryHost.cs" />
|
||||
<Compile Include="Models\IRepositoryHosts.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\AccountFactory.cs" />
|
||||
<Compile Include="Services\ApiClient.cs" />
|
||||
<Compile Include="Services\ApiClientFactory.cs" />
|
||||
<Compile Include="Services\Browser.cs" />
|
||||
<Compile Include="Services\EnterpriseProbe.cs" />
|
||||
<Compile Include="Services\GitHubCredentialStore.cs" />
|
||||
<Compile Include="Services\IAccountFactory.cs" />
|
||||
<Compile Include="Services\IApiClient.cs" />
|
||||
<Compile Include="Services\IApiClientFactory.cs" />
|
||||
<Compile Include="Services\IBrowser.cs" />
|
||||
<Compile Include="Services\IEnterpriseProbe.cs" />
|
||||
<Compile Include="Services\IRepositoryHostFactory.cs" />
|
||||
<Compile Include="Services\RepositoryHostFactory.cs" />
|
||||
<Compile Include="Services\StandardUserErrors.cs" />
|
||||
<Compile Include="ViewModels\LoginControlViewModel.cs" />
|
||||
<Compile Include="ViewModels\TwoFactorDialogViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
|
||||
<Project>{6559e128-8b40-49a5-85a8-05565ed0c7e3}</Project>
|
||||
<Name>GitHub.Extensions.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
|
||||
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
|
||||
<Name>GitHub.Extensions</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj">
|
||||
<Project>{158b05e8-fdbc-4d71-b871-c96e28d5adf5}</Project>
|
||||
<Name>GitHub.UI.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
|
||||
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
|
||||
<Name>Rothko</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets" Condition="Exists('..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets')" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
|
@ -0,0 +1,27 @@
|
|||
using System.IO;
|
||||
using Rothko;
|
||||
using Environment = System.Environment;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public static class EnvironmentExtensions
|
||||
{
|
||||
const string applicationName = "GitHub";
|
||||
|
||||
public static string GetLocalGitHubApplicationDataPath(this IEnvironment environment)
|
||||
{
|
||||
return Path.Combine(environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
applicationName);
|
||||
}
|
||||
|
||||
public static string GetApplicationDataPath(this IEnvironment environment)
|
||||
{
|
||||
return Path.Combine(environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), applicationName);
|
||||
}
|
||||
|
||||
public static string GetProgramFilesPath(this IEnvironment environment)
|
||||
{
|
||||
return environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace GitHub.Info
|
||||
{
|
||||
public static class GitHubUrls
|
||||
{
|
||||
/// <summary>
|
||||
/// https://github.com/
|
||||
/// </summary>
|
||||
public const string GitHub = "https://github.com/";
|
||||
|
||||
/// <summary>
|
||||
/// The url for a user's dashboard on github.com.
|
||||
/// </summary>
|
||||
public const string Dashboard = GitHub + "/dashboard";
|
||||
|
||||
/// <summary>
|
||||
/// The url for contacting support
|
||||
/// </summary>
|
||||
public const string ContactSupport = GitHub + "/contact";
|
||||
|
||||
/// <summary>
|
||||
/// The GitHub for Windows download and promo site
|
||||
/// </summary>
|
||||
public const string GitHubForWindows = "http://windows.github.com";
|
||||
|
||||
/// <summary>
|
||||
/// The url for a user's billing information.
|
||||
/// </summary>
|
||||
public const string UserBilling = GitHub + "/account/billing";
|
||||
|
||||
/// <summary>
|
||||
/// The url for viewing signup information and plans on github.com.
|
||||
/// This includes a specific referral_code so we can track people
|
||||
/// coming to the site from the Windows App.
|
||||
/// </summary>
|
||||
public static readonly Uri Plans = new Uri(GitHub + "/plans?referral_code=GitHubWindows");
|
||||
|
||||
/// <summary>
|
||||
/// The url for resetting your password on github
|
||||
/// </summary>
|
||||
public static readonly Uri ForgotPasswordPath = new Uri("/sessions/forgot_password", UriKind.Relative);
|
||||
|
||||
/// <summary>
|
||||
/// The url to learn about GitHub:Enterprise
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This doesn't change per enterprise repo.
|
||||
/// </remarks>
|
||||
public static readonly Uri GitHubEnterpriseWeb = new Uri("https://enterprise.github.com");
|
||||
|
||||
/// <summary>
|
||||
/// The GitHub maintained repo of common .gitignores.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This doesn't change per enterprise repo.
|
||||
/// </remarks>
|
||||
public const string GitIgnoreRepo = "https://github.com/github/gitignore";
|
||||
|
||||
/// <summary>
|
||||
/// GitHub Help link about .gitattributes and line endings.
|
||||
/// </summary>
|
||||
public const string GitAttributesHelp = "https://help.github.com/articles/dealing-with-line-endings";
|
||||
|
||||
/// <summary>
|
||||
/// The release notes page on the GitHub for Windows marketing site.
|
||||
/// </summary>
|
||||
public const string WindowsChangeLog = "https://windows.github.com/release-notes.html";
|
||||
|
||||
/// <summary>
|
||||
/// The url for viewing billing information associated with a GitHub account.
|
||||
/// </summary>
|
||||
public static string Billing(this IAccount account)
|
||||
{
|
||||
return account.IsUser
|
||||
? UserBilling
|
||||
: string.Format(CultureInfo.InvariantCulture,
|
||||
GitHub + "/organizations/{0}/settings/billing", account.Login);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using Splat;
|
||||
|
||||
namespace GitHub.Infrastructure
|
||||
{
|
||||
public class AppModeDetector : IModeDetector
|
||||
{
|
||||
public bool? InUnitTestRunner()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool? InDesignMode()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using Octokit.Internal;
|
||||
|
||||
namespace GitHub.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Since VS doesn't support dynamic component registration, we have to implement wrappers
|
||||
/// for types we don't control in order to export them.
|
||||
/// </summary>
|
||||
[Export(typeof(IHttpClient))]
|
||||
public class ExportedHttpClient : HttpClientAdapter
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reactive.Linq;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public class Account : ReactiveObject, IAccount
|
||||
{
|
||||
string email;
|
||||
readonly ObservableAsPropertyHelper<bool> isOnFreePlan;
|
||||
readonly ObservableAsPropertyHelper<bool> hasMaximumPrivateRepositoriesLeft;
|
||||
readonly ObservableAsPropertyHelper<bool> isGitHubStaff;
|
||||
string login;
|
||||
string name;
|
||||
int ownedPrivateRepos;
|
||||
long privateReposInPlan;
|
||||
bool isSiteAdmin;
|
||||
|
||||
public Account(IRepositoryHost host, User user)
|
||||
: this(host)
|
||||
{
|
||||
Id = user.Id;
|
||||
IsUser = true;
|
||||
|
||||
Update(user);
|
||||
}
|
||||
|
||||
public Account(IRepositoryHost host, Organization organization)
|
||||
: this(host)
|
||||
{
|
||||
Id = organization.Id;
|
||||
IsUser = false;
|
||||
|
||||
Update(organization);
|
||||
}
|
||||
|
||||
private Account(IRepositoryHost host)
|
||||
{
|
||||
Host = host;
|
||||
|
||||
isOnFreePlan = this.WhenAny(x => x.PrivateReposInPlan, x => x.Value == 0)
|
||||
.ToProperty(this, x => x.IsOnFreePlan);
|
||||
|
||||
hasMaximumPrivateRepositoriesLeft = this.WhenAny(
|
||||
x => x.OwnedPrivateRepos,
|
||||
x => x.PrivateReposInPlan,
|
||||
(owned, avalible) => owned.Value >= avalible.Value)
|
||||
.ToProperty(this, x => x.HasMaximumPrivateRepositories);
|
||||
|
||||
isGitHubStaff = this.WhenAny(x => x.IsSiteAdmin, x => x.Value)
|
||||
.Select(admin => admin && host.IsGitHub)
|
||||
.ToProperty(this, x => x.IsGitHubStaff);
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get { return email; }
|
||||
private set { this.RaiseAndSetIfChanged(ref email, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the user is a member of the GitHub staff.
|
||||
/// </summary>
|
||||
public bool IsGitHubStaff
|
||||
{
|
||||
get { return isGitHubStaff.Value; }
|
||||
}
|
||||
|
||||
public IRepositoryHost Host { get; private set; }
|
||||
|
||||
public int Id { get; private set; }
|
||||
|
||||
public bool IsEnterprise { get { return Host.IsEnterprise; } }
|
||||
|
||||
public bool IsGitHub { get { return Host.IsGitHub; } }
|
||||
|
||||
public bool IsLocal { get { return false; } }
|
||||
|
||||
public bool IsOnFreePlan
|
||||
{
|
||||
get { return isOnFreePlan.Value; }
|
||||
}
|
||||
|
||||
public bool HasMaximumPrivateRepositories
|
||||
{
|
||||
get { return hasMaximumPrivateRepositoriesLeft.Value; }
|
||||
}
|
||||
|
||||
public bool IsUser { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the user is an admin on the host (GitHub or Enterprise).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not confuse this with "IsStaff". This is true if the user is an admin
|
||||
/// on the site. IsStaff is true if that site is github.com.
|
||||
/// </remarks>
|
||||
public bool IsSiteAdmin
|
||||
{
|
||||
get { return isSiteAdmin; }
|
||||
private set { this.RaiseAndSetIfChanged(ref isSiteAdmin, value); }
|
||||
}
|
||||
|
||||
public string Login
|
||||
{
|
||||
get { return login; }
|
||||
private set { this.RaiseAndSetIfChanged(ref login, value); }
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return name; }
|
||||
private set { this.RaiseAndSetIfChanged(ref name, value); }
|
||||
}
|
||||
|
||||
public int OwnedPrivateRepos
|
||||
{
|
||||
get { return ownedPrivateRepos; }
|
||||
private set { this.RaiseAndSetIfChanged(ref ownedPrivateRepos, value); }
|
||||
}
|
||||
|
||||
public long PrivateReposInPlan
|
||||
{
|
||||
get { return privateReposInPlan; }
|
||||
private set { this.RaiseAndSetIfChanged(ref privateReposInPlan, value); }
|
||||
}
|
||||
|
||||
public void Update(User user)
|
||||
{
|
||||
IsSiteAdmin = user.SiteAdmin;
|
||||
UpdateAccountInfo(user);
|
||||
}
|
||||
|
||||
public void Update(Organization organization)
|
||||
{
|
||||
UpdateAccountInfo(organization);
|
||||
}
|
||||
|
||||
void UpdateAccountInfo(Octokit.Account githubAccount)
|
||||
{
|
||||
if (Id != githubAccount.Id) return;
|
||||
|
||||
Email = githubAccount.Email;
|
||||
Login = githubAccount.Login;
|
||||
Name = githubAccount.Name ?? githubAccount.Login;
|
||||
OwnedPrivateRepos = githubAccount.OwnedPrivateRepos;
|
||||
PrivateReposInPlan = (githubAccount.Plan == null ? 0 : githubAccount.Plan.PrivateRepos);
|
||||
}
|
||||
|
||||
internal string DebuggerDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Format(CultureInfo.InvariantCulture,
|
||||
"Account: Login: {0} Name: {1}, Id: {2} Id: ", Login, Name, Id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Akavache;
|
||||
using GitHub.Authentication;
|
||||
using NullGuard;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class DisconnectedRepositoryHost : ReactiveObject, IRepositoryHost
|
||||
{
|
||||
public DisconnectedRepositoryHost()
|
||||
{
|
||||
Address = HostAddress.Create(new Uri("https://null/"));
|
||||
Organizations = new ReactiveList<IAccount>();
|
||||
Accounts = new ReactiveList<IAccount>();
|
||||
}
|
||||
|
||||
public HostAddress Address { get; private set; }
|
||||
public IApiClient ApiClient { get; private set; }
|
||||
|
||||
[AllowNull]
|
||||
public IHostCache Cache
|
||||
{
|
||||
[return: AllowNull]
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public bool IsGitHub { get; private set; }
|
||||
public bool IsLoggedIn { get; private set; }
|
||||
public bool IsLoggingIn { get; private set; }
|
||||
public bool IsEnterprise { get; private set; }
|
||||
public bool IsLocal { get; private set; }
|
||||
public ReactiveList<IAccount> Organizations { get; private set; }
|
||||
public ReactiveList<IAccount> Accounts { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public IAccount User { get; private set; }
|
||||
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
|
||||
{
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> LogInFromCache()
|
||||
{
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
public IObservable<Unit> LogOut()
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh()
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System.Reflection;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
// Represents the currently executing program.
|
||||
public interface IProgram
|
||||
{
|
||||
AssemblyName AssemblyName { get; }
|
||||
string ExecutingAssemblyDirectory { get; }
|
||||
ProductHeaderValue ProductHeader { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Reactive;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Helpers;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public interface IRepositoryHost : IReactiveObject
|
||||
{
|
||||
HostAddress Address { get; }
|
||||
IApiClient ApiClient { get; }
|
||||
IHostCache Cache { get; }
|
||||
bool IsGitHub { get; }
|
||||
bool IsLoggedIn { get; }
|
||||
bool IsLoggingIn { get; }
|
||||
bool IsEnterprise { get; }
|
||||
bool IsLocal { get; }
|
||||
ReactiveList<IAccount> Organizations { get; }
|
||||
ReactiveList<IAccount> Accounts { get; }
|
||||
string Title { get; }
|
||||
IAccount User { get; }
|
||||
|
||||
IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password);
|
||||
IObservable<AuthenticationResult> LogInFromCache();
|
||||
IObservable<Unit> LogOut();
|
||||
IObservable<Unit> Refresh();
|
||||
IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using GitHub.Authentication;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public interface IRepositoryHosts : IReactiveObject
|
||||
{
|
||||
IRepositoryHost EnterpriseHost { get; set; }
|
||||
IRepositoryHost GitHubHost { get; }
|
||||
IRepositoryHost LocalRepositoriesHost { get; }
|
||||
IObservable<AuthenticationResult> LogInEnterpriseHost(
|
||||
HostAddress enterpriseHostAddress,
|
||||
string usernameOrEmail,
|
||||
string password);
|
||||
IObservable<AuthenticationResult> LogInGitHubHost(string usernameOrEmail, string password);
|
||||
IRepositoryHostFactory RepositoryHostFactory { get; }
|
||||
bool IsLoggedInToAnyHost { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class LocalRepositoriesAccount : ReactiveObject, IAccount
|
||||
{
|
||||
public LocalRepositoriesAccount(IRepositoryHost host)
|
||||
{
|
||||
Avatar = "pack://application:,,,/GitHub;component/Images/computer.png";
|
||||
Email = string.Empty;
|
||||
Id = 0;
|
||||
IsOnFreePlan = true;
|
||||
IsUser = true;
|
||||
Host = host;
|
||||
Login = "repositories";
|
||||
Name = "repositories";
|
||||
OwnedPrivateRepos = 0;
|
||||
PrivateReposInPlan = 0;
|
||||
IsSiteAdmin = false;
|
||||
IsGitHubStaff = false;
|
||||
}
|
||||
|
||||
public object Avatar { get; private set; }
|
||||
public string Email { get; private set; }
|
||||
public int Id { get; private set; }
|
||||
public bool IsEnterprise { get { return false; } }
|
||||
public bool IsGitHub { get { return false; } }
|
||||
public bool IsLocal { get { return true; } }
|
||||
public bool IsOnFreePlan { get; private set; }
|
||||
public bool HasMaximumPrivateRepositories { get; private set; }
|
||||
public bool IsUser { get; private set; }
|
||||
public bool IsSiteAdmin { get; private set; }
|
||||
public bool IsGitHubStaff { get; private set; }
|
||||
public IRepositoryHost Host { get; private set; }
|
||||
public string Login { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public int OwnedPrivateRepos { get; private set; }
|
||||
public long PrivateReposInPlan { get; private set; }
|
||||
|
||||
public void Update(User ghUser)
|
||||
{
|
||||
}
|
||||
|
||||
public void Update(Organization org)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using GitHub.Authentication;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class LocalRepositoriesHost : ReactiveObject, IRepositoryHost
|
||||
{
|
||||
readonly Lazy<IAccount> userThunk;
|
||||
|
||||
public LocalRepositoriesHost()
|
||||
{
|
||||
userThunk = new Lazy<IAccount>(() => new LocalRepositoriesAccount(this));
|
||||
|
||||
Address = null;
|
||||
ApiClient = null;
|
||||
Cache = null;
|
||||
IsGitHub = false;
|
||||
IsLoggedIn = true;
|
||||
IsLoggingIn = false;
|
||||
Organizations = new ReactiveList<IAccount>();
|
||||
Accounts = new ReactiveList<IAccount>();
|
||||
Title = "local";
|
||||
}
|
||||
|
||||
public HostAddress Address { get; private set; }
|
||||
public IApiClient ApiClient { get; private set; }
|
||||
public IHostCache Cache { get; private set; }
|
||||
public bool IsGitHub { get; private set; }
|
||||
public bool IsLoggedIn { get; private set; }
|
||||
public bool IsLoggingIn { get; private set; }
|
||||
public ReactiveList<IAccount> Organizations { get; private set; }
|
||||
public ReactiveList<IAccount> Accounts { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
|
||||
public IAccount User
|
||||
{
|
||||
get { return userThunk.Value; }
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
|
||||
{
|
||||
return Observable.Return(AuthenticationResult.Success);
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> LogInFromCache()
|
||||
{
|
||||
return Observable.Return(AuthenticationResult.Success);
|
||||
}
|
||||
|
||||
public IObservable<Unit> LogOut()
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh()
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
|
||||
{
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
bool isSelected;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get { return isSelected; }
|
||||
set { this.RaiseAndSetIfChanged(ref isSelected, value); }
|
||||
}
|
||||
|
||||
public bool IsEnterprise
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public bool IsLocal
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using GitHub.Models;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
// Represents the currently executing program.
|
||||
[Export(typeof(IProgram))]
|
||||
public class Program : IProgram
|
||||
{
|
||||
public Program()
|
||||
{
|
||||
var executingAssembly = typeof(Program).Assembly;
|
||||
AssemblyName = executingAssembly.GetName();
|
||||
ExecutingAssemblyDirectory = Path.GetDirectoryName(executingAssembly.Location);
|
||||
ProductHeader = new ProductHeaderValue("GitHubVS", AssemblyName.Version.ToString());
|
||||
}
|
||||
|
||||
public AssemblyName AssemblyName { get; private set; }
|
||||
public string ExecutingAssemblyDirectory { get; private set; }
|
||||
public ProductHeaderValue ProductHeader { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,520 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Extensions.Reactive;
|
||||
using GitHub.Services;
|
||||
using NLog;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
using Authorization = Octokit.Authorization;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public class RepositoryHost : ReactiveObject, IRepositoryHost
|
||||
{
|
||||
static readonly Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
bool isLoggedIn;
|
||||
bool isLoggingIn;
|
||||
bool isSelected;
|
||||
IAccount userAccount;
|
||||
readonly IAccountFactory accountFactory;
|
||||
|
||||
public RepositoryHost(
|
||||
IApiClient apiClient,
|
||||
IHostCache hostCache,
|
||||
ILoginCache loginCache,
|
||||
IAccountFactory accountFactory)
|
||||
{
|
||||
Debug.Assert(apiClient.HostAddress != null, "HostAddress of an api client shouldn't be null");
|
||||
Address = apiClient.HostAddress;
|
||||
ApiBaseUri = apiClient.HostAddress.ApiUri;
|
||||
ApiClient = apiClient;
|
||||
Debug.Assert(ApiBaseUri != null, "Mistakes were made. ApiClient must have non-null ApiBaseUri");
|
||||
IsGitHub = ApiBaseUri.Equals(Api.ApiClient.GitHubDotComApiBaseUri);
|
||||
Cache = hostCache;
|
||||
LoginCache = loginCache;
|
||||
this.accountFactory = accountFactory;
|
||||
|
||||
IsEnterprise = !IsGitHub;
|
||||
Organizations = new ReactiveList<IAccount>();
|
||||
Title = MakeTitle(ApiBaseUri);
|
||||
|
||||
Accounts = new ReactiveList<IAccount>();
|
||||
|
||||
this.WhenAny(x => x.IsLoggedIn, x => x.Value)
|
||||
.Where(loggedIn => loggedIn)
|
||||
.Subscribe(_ => OnUserLoggedIn(User));
|
||||
|
||||
this.WhenAny(x => x.IsLoggedIn, x => x.Value)
|
||||
.Skip(1) // so we don't log the account out on the initial evaluation of the WhenAny
|
||||
.Where(loggedIn => !loggedIn)
|
||||
.Subscribe(_ => OnUserLoggedOut());
|
||||
|
||||
this.WhenAny(x => x.Organizations.ItemsAdded, x => x.Value)
|
||||
.SelectMany(x => x)
|
||||
.Subscribe(OnOrgAdded);
|
||||
|
||||
this.WhenAny(x => x.Organizations.ItemsRemoved, x => x.Value)
|
||||
.SelectMany(x => x)
|
||||
.Subscribe(OnOrgRemoved);
|
||||
}
|
||||
|
||||
Uri ApiBaseUri { get; set; }
|
||||
|
||||
public HostAddress Address { get; private set; }
|
||||
public IApiClient ApiClient { get; private set; }
|
||||
|
||||
public IHostCache Cache { get; private set; }
|
||||
|
||||
public bool IsGitHub { get; private set; }
|
||||
|
||||
public bool IsEnterprise { get; private set; }
|
||||
|
||||
public bool IsLocal
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public bool IsLoggedIn
|
||||
{
|
||||
get { return isLoggedIn; }
|
||||
private set { this.RaiseAndSetIfChanged(ref isLoggedIn, value); }
|
||||
}
|
||||
|
||||
public bool IsLoggingIn
|
||||
{
|
||||
get { return isLoggingIn; }
|
||||
private set { this.RaiseAndSetIfChanged(ref isLoggingIn, value); }
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get { return isSelected; }
|
||||
set { this.RaiseAndSetIfChanged(ref isSelected, value); }
|
||||
}
|
||||
|
||||
public ReactiveList<IAccount> Organizations { get; private set; }
|
||||
|
||||
public string Title { get; private set; }
|
||||
|
||||
public IAccount User
|
||||
{
|
||||
get { return userAccount; }
|
||||
private set { this.RaiseAndSetIfChanged(ref userAccount, value); }
|
||||
}
|
||||
|
||||
public ReactiveList<IAccount> Accounts { get; private set; }
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public IObservable<AuthenticationResult> LogInFromCache()
|
||||
{
|
||||
IsLoggingIn = true;
|
||||
|
||||
return ApiClient.GetUser()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Catch<User, Exception>(ex =>
|
||||
{
|
||||
if (ex is AuthorizationException)
|
||||
{
|
||||
IsLoggingIn = false;
|
||||
log.Warn("Got an authorization exception", ex);
|
||||
return Observable.Return<User>(null);
|
||||
}
|
||||
return Cache.GetUser()
|
||||
.Catch<User, Exception>(e =>
|
||||
{
|
||||
IsLoggingIn = false;
|
||||
log.Warn("User does not exist in cache", e);
|
||||
return Observable.Return<User>(null);
|
||||
})
|
||||
.ObserveOn(RxApp.MainThreadScheduler);
|
||||
})
|
||||
.SelectMany(LoginWithApiUser)
|
||||
.Do(result =>
|
||||
{
|
||||
if (result.IsFailure()) return;
|
||||
AddCachedOrganizations();
|
||||
})
|
||||
.PublishAsync();
|
||||
}
|
||||
|
||||
static readonly User unverifiedUser = new User { Id = int.MinValue };
|
||||
|
||||
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(usernameOrEmail, "usernameOrEmail");
|
||||
Guard.ArgumentNotEmptyString(password, "password");
|
||||
|
||||
// If we need to retry on fallback, we'll store the 2FA token
|
||||
// from the first request to re-use:
|
||||
string authenticationCode = null;
|
||||
|
||||
// We need to intercept the 2FA handler to get the token:
|
||||
var interceptingTwoFactorChallengeHandler =
|
||||
new Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>>(ex =>
|
||||
ApiClient.TwoFactorChallengeHandler.HandleTwoFactorException(ex)
|
||||
.Do(twoFactorChallengeResult =>
|
||||
authenticationCode = twoFactorChallengeResult.AuthenticationCode));
|
||||
|
||||
// Keep the function to save the authorization token here because it's used
|
||||
// in multiple places in the chain below:
|
||||
var saveAuthorizationToken = new Func<Authorization, IObservable<Unit>>(authorization =>
|
||||
{
|
||||
if (authorization == null || String.IsNullOrWhiteSpace(authorization.Token))
|
||||
return Observable.Return(Unit.Default);
|
||||
|
||||
return LoginCache.SaveLogin(authorization.Token, "x-oauth-basic", Address)
|
||||
.ObserveOn(RxApp.MainThreadScheduler);
|
||||
});
|
||||
|
||||
// Start be saving the username and password, as they will be used for older versions of Enterprise
|
||||
// that don't support authorization tokens, and for the API client to use until an authorization
|
||||
// token has been created and acquired:
|
||||
return LoginCache.SaveLogin(usernameOrEmail, password, Address)
|
||||
.Do(_ => IsLoggingIn = true)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
// Try to get an authorization token, save it, then get the user to log in:
|
||||
.SelectMany(_ => ApiClient.GetOrCreateApplicationAuthenticationCode(interceptingTwoFactorChallengeHandler))
|
||||
.SelectMany(saveAuthorizationToken)
|
||||
.SelectMany(_ => ApiClient.GetUser())
|
||||
.Catch<User, ApiException>(firstTryEx =>
|
||||
{
|
||||
var exception = firstTryEx as AuthorizationException;
|
||||
if (IsEnterprise
|
||||
&& exception != null
|
||||
&& exception.Message == "Bad credentials")
|
||||
{
|
||||
return Observable.Throw<User>(exception);
|
||||
}
|
||||
|
||||
// If the Enterprise host doesn't support the write:public_key scope, it'll return a 422.
|
||||
// EXCEPT, there's a bug where it doesn't, and instead creates a bad token, and in
|
||||
// that case we'd get a 401 here from the GetUser invocation. So to be safe (and consistent
|
||||
// with the Mac app), we'll just retry after any API error for Enterprise hosts:
|
||||
if (IsEnterprise && !(firstTryEx is TwoFactorChallengeFailedException))
|
||||
{
|
||||
// Because we potentially have a bad authorization token due to the Enterprise bug,
|
||||
// we need to reset to using username and password authentication:
|
||||
return LoginCache.SaveLogin(usernameOrEmail, password, Address)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.SelectMany(_ =>
|
||||
{
|
||||
// Retry with the old scopes. If we have a stashed 2FA token, we use it:
|
||||
if (authenticationCode != null)
|
||||
return ApiClient.GetOrCreateApplicationAuthenticationCode(
|
||||
authenticationCode,
|
||||
true);
|
||||
|
||||
// Otherwise, we use the default handler:
|
||||
return ApiClient.GetOrCreateApplicationAuthenticationCode(useOldScopes: true);
|
||||
})
|
||||
// Then save the authorization token (if there is one) and get the user:
|
||||
.SelectMany(saveAuthorizationToken)
|
||||
.SelectMany(_ => ApiClient.GetUser());
|
||||
}
|
||||
|
||||
return Observable.Throw<User>(firstTryEx);
|
||||
})
|
||||
.Catch<User, ApiException>(retryEx =>
|
||||
{
|
||||
// Older Enterprise hosts either don't have the API end-point to PUT an authorization, or they
|
||||
// return 422 because they haven't white-listed our client ID. In that case, we just ignore
|
||||
// the failure, using basic authentication (with username and password) instead of trying
|
||||
// to get an authorization token.
|
||||
if (IsEnterprise && (retryEx is NotFoundException || retryEx.StatusCode == (HttpStatusCode)422))
|
||||
return ApiClient.GetUser();
|
||||
|
||||
// Other errors are "real" so we pass them along:
|
||||
return Observable.Throw<User>(retryEx);
|
||||
})
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Catch<User, Exception>(ex =>
|
||||
{
|
||||
// If we get here, we have an actual login failure:
|
||||
IsLoggingIn = false;
|
||||
if (ex is TwoFactorChallengeFailedException)
|
||||
{
|
||||
return Observable.Return(unverifiedUser);
|
||||
}
|
||||
if (ex is AuthorizationException)
|
||||
{
|
||||
return Observable.Return(default(User));
|
||||
}
|
||||
return Observable.Throw<User>(ex);
|
||||
})
|
||||
.SelectMany(LoginWithApiUser)
|
||||
.Do(result =>
|
||||
{
|
||||
if (result.IsFailure()) return;
|
||||
RefreshOrgs().Subscribe(
|
||||
_ => { },
|
||||
ex => log.Warn("Failed to refresh orgs.", ex));
|
||||
})
|
||||
.PublishAsync();
|
||||
}
|
||||
|
||||
IObservable<AuthenticationResult> LoginWithApiUser(User user)
|
||||
{
|
||||
return Observable.Start(() =>
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
IsLoggingIn = false;
|
||||
return AuthenticationResult.CredentialFailure;
|
||||
}
|
||||
if (user == unverifiedUser)
|
||||
{
|
||||
IsLoggingIn = false;
|
||||
LoginCache.EraseLogin(Address);
|
||||
return AuthenticationResult.VerificationFailure;
|
||||
}
|
||||
|
||||
Cache.InsertUser(user);
|
||||
User = accountFactory.CreateAccount(this, user);
|
||||
IsLoggedIn = true;
|
||||
IsLoggingIn = false;
|
||||
return AuthenticationResult.Success;
|
||||
})
|
||||
.Do(result => log.Info("Log in from cache for login '{0}' to host '{1}' {2}",
|
||||
user != null ? user.Login : "(null)",
|
||||
ApiBaseUri,
|
||||
result.IsSuccess() ? "SUCCEEDED" : "FAILED"))
|
||||
.PublishAsync();
|
||||
}
|
||||
|
||||
public IObservable<Unit> LogOut()
|
||||
{
|
||||
if (!IsLoggedIn) return Observable.Return(Unit.Default);
|
||||
|
||||
log.Info(CultureInfo.InvariantCulture, "Logged user {0} off of host '{1}'", User.Login, ApiBaseUri);
|
||||
|
||||
return LoginCache.EraseLogin(Address)
|
||||
.Catch<Unit, Exception>(e =>
|
||||
{
|
||||
log.Warn("ASSERT! Failed to erase login. Going to invalidate cache anyways.", e);
|
||||
return Observable.Return(Unit.Default);
|
||||
})
|
||||
.SelectMany(_ => Cache.InvalidateAll())
|
||||
.Catch<Unit, Exception>(e =>
|
||||
{
|
||||
log.Warn("ASSERT! Failed to invaldiate caches", e);
|
||||
return Observable.Return(Unit.Default);
|
||||
})
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Finally(() =>
|
||||
{
|
||||
IsLoggedIn = false;
|
||||
Organizations.Clear();
|
||||
User = null;
|
||||
});
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh()
|
||||
{
|
||||
return Refresh(h => Observable.Return(Unit.Default));
|
||||
}
|
||||
|
||||
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
|
||||
{
|
||||
if (!IsLoggedIn) return Observable.Return(Unit.Default);
|
||||
|
||||
try
|
||||
{
|
||||
return Observable.Merge(
|
||||
RefreshUser(),
|
||||
RefreshOrgs())
|
||||
.AsCompletion()
|
||||
.SelectMany(_ => refreshTrackedRepositoriesFunc(this))
|
||||
.Catch<Unit, Exception>(ex =>
|
||||
{
|
||||
log.Warn("Refresh failed.", ex);
|
||||
return ex.ShowUserErrorMessage(ErrorType.RefreshFailed)
|
||||
.AsCompletion();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Warn("Repository host refresh failed.", ex);
|
||||
return ex.ShowUserErrorMessage(ErrorType.RefreshFailed)
|
||||
.AsCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
protected ILoginCache LoginCache { get; private set; }
|
||||
|
||||
void AddCachedOrganizations()
|
||||
{
|
||||
Cache.GetAllOrganizations()
|
||||
.Select(orgs => orgs.Where(o => o != null))
|
||||
.Where(orgs => orgs.Any())
|
||||
.Select(orgs => orgs.OrderBy(org => org.Login))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(
|
||||
AddCachedOrganizations,
|
||||
ex => log.Warn("Failed to get orgs from cache.", ex));
|
||||
}
|
||||
|
||||
void AddCachedOrganizations(IEnumerable<Organization> ghOrgs)
|
||||
{
|
||||
if (ghOrgs == null) return;
|
||||
|
||||
var orgs = ghOrgs.Select(ghOrg => accountFactory.CreateAccount(this, ghOrg))
|
||||
.Except(Organizations, (first, second) => first.Id == second.Id); // only add from cache if not already there
|
||||
|
||||
// instead of AddRange here, we need to add the
|
||||
// items one at a time so the ItemAdded and ItemRemoved
|
||||
// signals are raised for the account list
|
||||
foreach (var org in orgs)
|
||||
{
|
||||
Organizations.Add(org);
|
||||
}
|
||||
}
|
||||
|
||||
void AddOrUpdateOrg(IAccount org)
|
||||
{
|
||||
if (org == null) return;
|
||||
|
||||
var existingOrg = Organizations.SingleOrDefault(x => x.Id == org.Id);
|
||||
if (existingOrg == null)
|
||||
{
|
||||
Organizations.Add(org);
|
||||
return;
|
||||
}
|
||||
Organizations[Organizations.IndexOf(existingOrg)] = org;
|
||||
}
|
||||
|
||||
static string MakeTitle(Uri apiBaseUri)
|
||||
{
|
||||
return apiBaseUri.Equals(Api.ApiClient.GitHubDotComApiBaseUri) ?
|
||||
"github" :
|
||||
apiBaseUri.Host;
|
||||
}
|
||||
|
||||
void RefreshOrgs(ICollection<IAccount> orgs)
|
||||
{
|
||||
orgs.ForEach(org =>
|
||||
{
|
||||
AddOrUpdateOrg(org);
|
||||
|
||||
ApiClient.GetOrganization(org.Login)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(
|
||||
ghOrg =>
|
||||
{
|
||||
Cache.InsertOrganization(ghOrg);
|
||||
UpdateOrg(ghOrg);
|
||||
},
|
||||
ex => log.Warn("Failed to get organization.", ex));
|
||||
});
|
||||
|
||||
var orgsToRemove = Organizations.Except(orgs, (x, y) => x.Id == y.Id).ToArray();
|
||||
|
||||
orgsToRemove.ForEach(orgToRemove => Cache.InvalidateOrganization(orgToRemove));
|
||||
|
||||
RemoveOrgs(orgsToRemove);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
IObservable<Unit> RefreshOrgs()
|
||||
{
|
||||
return ApiClient.GetOrganizations()
|
||||
.WhereNotNull()
|
||||
.ToArray()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Select(ghOrgs => ghOrgs
|
||||
.OrderBy(ghOrg => ghOrg.Login)
|
||||
.Select(org => accountFactory.CreateAccount(this, org))
|
||||
.ToArray())
|
||||
.Select(ghOrgs =>
|
||||
{
|
||||
RefreshOrgs(ghOrgs);
|
||||
return Unit.Default;
|
||||
})
|
||||
.PublishAsync();
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
IObservable<Unit> RefreshUser()
|
||||
{
|
||||
return ApiClient.GetUser()
|
||||
.WhereNotNull()
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Select(ghUser =>
|
||||
{
|
||||
Cache.InsertUser(ghUser);
|
||||
if (User != null)
|
||||
User.Update(ghUser);
|
||||
|
||||
return Unit.Default;
|
||||
})
|
||||
.PublishAsync();
|
||||
}
|
||||
|
||||
void RemoveOrgs(IEnumerable<IAccount> orgs)
|
||||
{
|
||||
orgs.ForEach(org =>
|
||||
{
|
||||
var orgToRemove = Organizations.SingleOrDefault(o => org.Id == o.Id);
|
||||
if (orgToRemove != null)
|
||||
Organizations.Remove(orgToRemove);
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateOrg(Organization ghOrg)
|
||||
{
|
||||
if (ghOrg == null) return;
|
||||
|
||||
var existingOrg = Organizations.SingleOrDefault(x => x.Id == ghOrg.Id);
|
||||
if (existingOrg != null)
|
||||
existingOrg.Update(ghOrg);
|
||||
}
|
||||
|
||||
|
||||
void OnOrgAdded(IAccount account)
|
||||
{
|
||||
var accountTile = Accounts.SingleOrDefault(x => x != null && x.Id == account.Id);
|
||||
|
||||
if (accountTile == null)
|
||||
{
|
||||
Accounts.Add(account);
|
||||
}
|
||||
}
|
||||
|
||||
void OnOrgRemoved(IAccount account)
|
||||
{
|
||||
var accountTile = Accounts.SingleOrDefault(x => x != null && x.Id == account.Id);
|
||||
|
||||
if (accountTile == null) return;
|
||||
|
||||
Accounts.Remove(accountTile);
|
||||
}
|
||||
|
||||
void OnUserLoggedIn(IAccount account)
|
||||
{
|
||||
Accounts.Insert(0, account);
|
||||
}
|
||||
|
||||
void OnUserLoggedOut()
|
||||
{
|
||||
Accounts.Clear();
|
||||
}
|
||||
|
||||
internal string DebuggerDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Format(CultureInfo.InvariantCulture, "RepositoryHost: {0} {1}", Title, ApiBaseUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Akavache;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Extensions.Reactive;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
[Export(typeof(IRepositoryHosts))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class RepositoryHosts : ReactiveObject, IRepositoryHosts, IDisposable
|
||||
{
|
||||
static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
public static DisconnectedRepositoryHost DisconnectedRepositoryHost = new DisconnectedRepositoryHost();
|
||||
public const string EnterpriseHostApiBaseUriCacheKey = "enterprise-host-api-base-uri";
|
||||
readonly ObservableAsPropertyHelper<bool> isLoggedInToAnyHost;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryHosts(
|
||||
IRepositoryHostFactory repositoryHostFactory,
|
||||
ISharedCache sharedCache)
|
||||
{
|
||||
RepositoryHostFactory = repositoryHostFactory;
|
||||
|
||||
LocalRepositoriesHost = new LocalRepositoriesHost();
|
||||
GitHubHost = repositoryHostFactory.Create(HostAddress.GitHubDotComHostAddress);
|
||||
EnterpriseHost = DisconnectedRepositoryHost;
|
||||
|
||||
var initialCacheLoadObs = sharedCache.UserAccount.GetObject<Uri>(EnterpriseHostApiBaseUriCacheKey)
|
||||
.Catch<Uri, KeyNotFoundException>(_ => Observable.Return<Uri>(null))
|
||||
.Catch<Uri, Exception>(ex =>
|
||||
{
|
||||
log.Warn("Failed to get Enterprise host URI from cache.", ex);
|
||||
return Observable.Return<Uri>(null);
|
||||
})
|
||||
.WhereNotNull()
|
||||
.Select(HostAddress.Create)
|
||||
.Select(repositoryHostFactory.Create)
|
||||
.Do(x => EnterpriseHost = x)
|
||||
.SelectUnit();
|
||||
|
||||
var persistEntepriseHostObs = this.WhenAny(x => x.EnterpriseHost, x => x.Value)
|
||||
.Skip(1) // The first value will be null or something already in the db
|
||||
.SelectMany(enterpriseHost =>
|
||||
{
|
||||
if (!enterpriseHost.IsLoggedIn)
|
||||
{
|
||||
return sharedCache.UserAccount
|
||||
.InvalidateObject<Uri>(EnterpriseHostApiBaseUriCacheKey)
|
||||
.Catch<Unit, Exception>(ex =>
|
||||
{
|
||||
log.Warn("Failed to invalidate enterprise host uri", ex);
|
||||
return Observable.Return(Unit.Default);
|
||||
});
|
||||
}
|
||||
|
||||
return sharedCache.UserAccount
|
||||
.InsertObject(EnterpriseHostApiBaseUriCacheKey, enterpriseHost.Address.ApiUri)
|
||||
.Catch<Unit, Exception>(ex =>
|
||||
{
|
||||
log.Warn("Failed to persist enterprise host uri", ex);
|
||||
return Observable.Return(Unit.Default);
|
||||
});
|
||||
});
|
||||
|
||||
isLoggedInToAnyHost = this.WhenAny(
|
||||
x => x.GitHubHost.IsLoggedIn,
|
||||
x => x.EnterpriseHost.IsLoggedIn,
|
||||
(githubLoggedIn, enterpriseLoggedIn) => githubLoggedIn.Value || enterpriseLoggedIn.Value)
|
||||
.ToProperty(this, x => x.IsLoggedInToAnyHost);
|
||||
|
||||
// Wait until we've loaded (or failed to load) an enterprise uri from the db and then
|
||||
// start tracking changes to the EntepriseHost property and persist every change to the db
|
||||
Observable.Concat(initialCacheLoadObs, persistEntepriseHostObs).Subscribe();
|
||||
}
|
||||
|
||||
IRepositoryHost githubHost;
|
||||
public IRepositoryHost GitHubHost
|
||||
{
|
||||
get { return githubHost; }
|
||||
private set { this.RaiseAndSetIfChanged(ref githubHost, value); }
|
||||
}
|
||||
|
||||
IRepositoryHost enterpriseHost;
|
||||
public IRepositoryHost EnterpriseHost
|
||||
{
|
||||
get { return enterpriseHost; }
|
||||
set
|
||||
{
|
||||
var newHost = value ?? DisconnectedRepositoryHost;
|
||||
this.RaiseAndSetIfChanged(ref enterpriseHost, newHost);
|
||||
}
|
||||
}
|
||||
|
||||
IRepositoryHost localRepositoriesHost;
|
||||
public IRepositoryHost LocalRepositoriesHost
|
||||
{
|
||||
get { return localRepositoriesHost; }
|
||||
set { this.RaiseAndSetIfChanged(ref localRepositoriesHost, value); }
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> LogInEnterpriseHost(
|
||||
HostAddress enterpriseHostAddress,
|
||||
string usernameOrEmail,
|
||||
string password)
|
||||
{
|
||||
var host = RepositoryHostFactory.Create(enterpriseHostAddress);
|
||||
return host.LogIn(usernameOrEmail, password)
|
||||
.Catch<AuthenticationResult, Exception>(Observable.Throw<AuthenticationResult>)
|
||||
.Do(result =>
|
||||
{
|
||||
bool successful = result.IsSuccess();
|
||||
log.Info("Log in to Enterprise host '{0}' with username '{1}' {2}",
|
||||
enterpriseHostAddress.ApiUri,
|
||||
usernameOrEmail,
|
||||
successful ? "SUCCEEDED" : "FAILED");
|
||||
if (successful)
|
||||
{
|
||||
EnterpriseHost = host;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> LogInGitHubHost(string usernameOrEmail, string password)
|
||||
{
|
||||
return GitHubHost.LogIn(usernameOrEmail, password)
|
||||
.Catch<AuthenticationResult, Exception>(Observable.Throw<AuthenticationResult>)
|
||||
.Do(result => log.Info("Log in to GitHub.com with username '{0}' {1}",
|
||||
usernameOrEmail,
|
||||
result.IsSuccess() ? "SUCCEEDED" : "FAILED"));
|
||||
}
|
||||
|
||||
public IRepositoryHostFactory RepositoryHostFactory { get; private set; }
|
||||
|
||||
public bool IsLoggedInToAnyHost { get { return isLoggedInToAnyHost.Value; } }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && EnterpriseHost.Cache != null)
|
||||
{
|
||||
EnterpriseHost.Cache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using GitHub.Extensions;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public class HostAddress
|
||||
{
|
||||
public static HostAddress GitHubDotComHostAddress = new HostAddress();
|
||||
static readonly Uri gistUri = new Uri("https://gist.github.com");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a host address based on the hostUri based on the expected patterns for GitHub.com and
|
||||
/// GitHub Enterprise instances. The passed in URI can be any URL to the instance.
|
||||
/// </summary>
|
||||
/// <param name="hostUri">The URI to a GitHub or GitHub Enterprise instance.</param>
|
||||
/// <returns></returns>
|
||||
public static HostAddress Create(Uri hostUri)
|
||||
{
|
||||
return IsGitHubDotComUri(hostUri)
|
||||
? GitHubDotComHostAddress
|
||||
: new HostAddress(hostUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a host address from a host name or URL as a string.
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <returns></returns>
|
||||
public static HostAddress Create(string host)
|
||||
{
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(host, UriKind.Absolute, out uri)
|
||||
|| Uri.TryCreate("https://" + host, UriKind.Absolute, out uri))
|
||||
{
|
||||
return Create(uri);
|
||||
}
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture,
|
||||
"The host '{0}' is not a valid host",
|
||||
host));
|
||||
}
|
||||
|
||||
private HostAddress(Uri enterpriseUri)
|
||||
{
|
||||
WebUri = new Uri(enterpriseUri, new Uri("/", UriKind.Relative));
|
||||
ApiUri = new Uri(enterpriseUri, new Uri("/api/v3/", UriKind.Relative));
|
||||
CredentialCacheKeyHost = ApiUri.Host;
|
||||
}
|
||||
|
||||
private HostAddress()
|
||||
{
|
||||
WebUri = new Uri("https://github.com");
|
||||
ApiUri = new Uri("https://api.github.com");
|
||||
CredentialCacheKeyHost = "github.com";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Base URL to the host. For example, "https://github.com" or "https://ghe.io"
|
||||
/// </summary>
|
||||
public Uri WebUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Base Url to the host's API endpoint. For example, "https://api.github.com" or
|
||||
/// "https://ghe.io/api/v3"
|
||||
/// </summary>
|
||||
public Uri ApiUri { get; set; }
|
||||
|
||||
// If the host name is "api.github.com" or "gist.github.com", we really only want "github.com",
|
||||
// since that's the same cache key for all the other github.com operations.
|
||||
public string CredentialCacheKeyHost { get; private set; }
|
||||
|
||||
static bool IsGitHubDotComUri(Uri hostUri)
|
||||
{
|
||||
return hostUri.IsSameHost(GitHubDotComHostAddress.WebUri)
|
||||
|| hostUri.IsSameHost(GitHubDotComHostAddress.ApiUri)
|
||||
|| hostUri.IsSameHost(gistUri);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("GitHub.App")]
|
||||
[assembly: AssemblyDescription("Provides the view models for the GitHub for Visual Studio extension")]
|
||||
[assembly: Guid("a8b9a236-d238-4733-b116-716872a1e8e0")]
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using GitHub.Models;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(IAccountFactory))]
|
||||
public class AccountFactory : IAccountFactory
|
||||
{
|
||||
public IAccount CreateAccount(
|
||||
IRepositoryHost repositoryHost,
|
||||
Octokit.User user)
|
||||
{
|
||||
return new Account(repositoryHost, user);
|
||||
}
|
||||
|
||||
public IAccount CreateAccount(
|
||||
IRepositoryHost repositoryHost,
|
||||
Octokit.Organization organization)
|
||||
{
|
||||
return new Account(repositoryHost, organization);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
using System.Reactive.Linq;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Extensions;
|
||||
using NLog;
|
||||
using Octokit;
|
||||
using Octokit.Reactive;
|
||||
using ReactiveUI;
|
||||
using Authorization = Octokit.Authorization;
|
||||
using LogManager = NLog.LogManager;
|
||||
|
||||
namespace GitHub.Api
|
||||
{
|
||||
public class ApiClient : IApiClient
|
||||
{
|
||||
static readonly Logger log = LogManager.GetCurrentClassLogger();
|
||||
/// <summary>
|
||||
/// https://github.com/
|
||||
/// </summary>
|
||||
public const string GitHubUrl = "https://" + GitHubDotComHostName;
|
||||
public const string GitHubDotComHostName = "github.com";
|
||||
public const string GitHubGistHostName = "gist.github.com";
|
||||
const string clientId = "";
|
||||
const string clientSecret = "";
|
||||
public static readonly Uri GitHubDotComUri = new Uri(GitHubUrl);
|
||||
public static readonly Uri GitHubDotComApiBaseUri = new Uri("https://api." + GitHubDotComHostName);
|
||||
|
||||
readonly IObservableGitHubClient gitHubClient;
|
||||
// There are two sets of authorization scopes, old and new:
|
||||
// The old scops must be used by older versions of Enterprise that don't support the new scopes:
|
||||
readonly string[] oldAuthorizationScopes = { "user", "repo" };
|
||||
// These new scopes include write:public_key, which allows us to add public SSH keys to an account:
|
||||
readonly string[] newAuthorizationScopes = { "user", "repo", "write:public_key" };
|
||||
|
||||
public ApiClient(
|
||||
HostAddress hostAddress,
|
||||
IObservableGitHubClient gitHubClient,
|
||||
ITwoFactorChallengeHandler twoFactorChallengeHandler)
|
||||
{
|
||||
HostAddress = hostAddress;
|
||||
this.gitHubClient = gitHubClient;
|
||||
TwoFactorChallengeHandler = twoFactorChallengeHandler;
|
||||
}
|
||||
|
||||
public HostAddress HostAddress { get; private set; }
|
||||
|
||||
public IObservable<Repository> CreateRepository(Repository repo, string login, bool isUser)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(login, "login");
|
||||
|
||||
var newRepository = ToNewRepository(repo);
|
||||
var client = gitHubClient.Repository;
|
||||
|
||||
return (isUser ? client.Create(newRepository) : client.Create(login, newRepository));
|
||||
}
|
||||
|
||||
public IObservable<SshKey> GetSshKeys()
|
||||
{
|
||||
return gitHubClient.SshKey.GetAllForCurrent();
|
||||
}
|
||||
|
||||
public IObservable<SshKey> AddSshKey(SshKey newKey)
|
||||
{
|
||||
log.Info("About to add SSH Key: {0} - {1}", newKey.Title, newKey.Key);
|
||||
|
||||
return gitHubClient.SshKey.Create(new SshKeyUpdate { Title = newKey.Title, Key = newKey.Key });
|
||||
}
|
||||
|
||||
public IObservable<User> GetUser()
|
||||
{
|
||||
return gitHubClient.User.Current();
|
||||
}
|
||||
|
||||
public IObservable<User> GetAllUsersForAllOrganizations()
|
||||
{
|
||||
return GetOrganizations().SelectMany(org => gitHubClient.Organization.Member.GetAll(org.Login));
|
||||
}
|
||||
|
||||
public IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander = null, bool useOldScopes = false)
|
||||
{
|
||||
var newAuthorization = new NewAuthorization
|
||||
{
|
||||
Scopes = useOldScopes
|
||||
? oldAuthorizationScopes
|
||||
: newAuthorizationScopes,
|
||||
Note = "GitHub for Windows on " + GetMachineNameSafe()
|
||||
};
|
||||
|
||||
var handler = twoFactorChallengeHander ?? TwoFactorChallengeHandler.HandleTwoFactorException;
|
||||
|
||||
Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> dispatchedHandler =
|
||||
ex => Observable.Start(() => handler(ex), RxApp.MainThreadScheduler).SelectMany(result => result);
|
||||
|
||||
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
|
||||
clientId,
|
||||
clientSecret,
|
||||
newAuthorization,
|
||||
dispatchedHandler);
|
||||
}
|
||||
|
||||
public IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
|
||||
string authenticationCode,
|
||||
bool useOldScopes = false)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(authenticationCode, "authenticationCode");
|
||||
|
||||
var newAuthorization = new NewAuthorization
|
||||
{
|
||||
Scopes = useOldScopes
|
||||
? oldAuthorizationScopes
|
||||
: newAuthorizationScopes,
|
||||
Note = "GitHub for Windows on " + GetMachineNameSafe()
|
||||
};
|
||||
|
||||
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
|
||||
clientId,
|
||||
clientSecret,
|
||||
newAuthorization,
|
||||
authenticationCode);
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyList<EmailAddress>> GetEmails()
|
||||
{
|
||||
return gitHubClient.User.Email.GetAll()
|
||||
.ToArray()
|
||||
.Select(emails => new ReadOnlyCollection<EmailAddress>(emails));
|
||||
}
|
||||
|
||||
public IObservable<Organization> GetOrganization(string login)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(login, "login");
|
||||
return gitHubClient.Organization.Get(login);
|
||||
}
|
||||
|
||||
public IObservable<Organization> GetOrganizations()
|
||||
{
|
||||
return gitHubClient.Organization.GetAllForCurrent();
|
||||
}
|
||||
|
||||
public IObservable<User> GetMembersOfOrganization(string organizationName)
|
||||
{
|
||||
return gitHubClient.Organization.Member.GetAll(organizationName);
|
||||
}
|
||||
|
||||
IObservable<IEnumerable<Repository>> GetAllRepositoriesForOrganization(Organization org)
|
||||
{
|
||||
return gitHubClient.Repository.GetAllForOrg(org.Login)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
IObservable<IEnumerable<Repository>> GetAllRepositoriesForCurrentUser()
|
||||
{
|
||||
return gitHubClient.Repository.GetAllForCurrent()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IObservable<Repository> GetCurrentUserRepositoriesStreamed()
|
||||
{
|
||||
return gitHubClient.Repository.GetAllForCurrent();
|
||||
}
|
||||
|
||||
public IObservable<Repository> GetOrganizationRepositoriesStreamed(string login)
|
||||
{
|
||||
return gitHubClient.Repository.GetAllForOrg(login);
|
||||
}
|
||||
|
||||
public IObservable<Repository> GetRepository(string owner, string name)
|
||||
{
|
||||
return gitHubClient.Repository.Get(owner, name);
|
||||
}
|
||||
|
||||
public IObservable<IEnumerable<Repository>> GetUserRepositories(int currentUserId)
|
||||
{
|
||||
return Observable.Merge(
|
||||
GetAllRepositoriesForCurrentUser(),
|
||||
GetOrganizations().SelectMany(GetAllRepositoriesForOrganization)
|
||||
);
|
||||
}
|
||||
|
||||
static NewRepository ToNewRepository(Repository repository)
|
||||
{
|
||||
return new NewRepository
|
||||
{
|
||||
Name = repository.Name,
|
||||
Description = repository.Description,
|
||||
Homepage = repository.Homepage,
|
||||
Private = repository.Private,
|
||||
HasIssues = repository.HasIssues,
|
||||
HasWiki = repository.HasWiki,
|
||||
HasDownloads= repository.HasDownloads
|
||||
};
|
||||
}
|
||||
|
||||
static string GetMachineNameSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Dns.GetHostName();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Environment.MachineName;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return "(unknown)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Models;
|
||||
using Octokit;
|
||||
using Octokit.Reactive;
|
||||
|
||||
namespace GitHub.Api
|
||||
{
|
||||
[Export(typeof(IApiClientFactory))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class ApiClientFactory : IApiClientFactory
|
||||
{
|
||||
readonly ITwoFactorChallengeHandler twoFactorChallengeHandler;
|
||||
readonly ProductHeaderValue productHeader;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ApiClientFactory(
|
||||
ILoginCache loginCache,
|
||||
ITwoFactorChallengeHandler twoFactorChallengeHandler,
|
||||
IProgram program)
|
||||
{
|
||||
LoginCache = loginCache;
|
||||
this.twoFactorChallengeHandler = twoFactorChallengeHandler;
|
||||
productHeader = program.ProductHeader;
|
||||
}
|
||||
|
||||
public IApiClient Create(HostAddress hostAddress)
|
||||
{
|
||||
var apiBaseUri = hostAddress.ApiUri;
|
||||
|
||||
return new ApiClient(
|
||||
hostAddress,
|
||||
new ObservableGitHubClient(
|
||||
new GitHubClient(productHeader, new GitHubCredentialStore(hostAddress, LoginCache), apiBaseUri)),
|
||||
twoFactorChallengeHandler);
|
||||
}
|
||||
|
||||
protected ILoginCache LoginCache { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using GitHub.Services;
|
||||
using NLog;
|
||||
using Rothko;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(IBrowser))]
|
||||
public class Browser : IBrowser
|
||||
{
|
||||
static readonly Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly IProcessStarter processManager;
|
||||
readonly IEnvironment environment;
|
||||
|
||||
[ImportingConstructor]
|
||||
public Browser(IProcessStarter processManager, IEnvironment environment)
|
||||
{
|
||||
this.processManager = processManager;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
public void OpenUrl(Uri url)
|
||||
{
|
||||
if (url == null)
|
||||
{
|
||||
log.Warn("Attempted to open a null URL");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
processManager.Start(url.ToString(), string.Empty);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Warn("Opening URL in default browser failed", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
processManager.Start(
|
||||
Path.Combine(environment.GetProgramFilesPath(), @"Internet Explorer", "iexplore.exe"),
|
||||
url.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("Really can't open the URL, even in IE", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using Octokit;
|
||||
using Octokit.Internal;
|
||||
|
||||
namespace GitHub.Helpers
|
||||
{
|
||||
[Export(typeof(IEnterpriseProbe))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class EnterpriseProbe : IEnterpriseProbe
|
||||
{
|
||||
static readonly Uri endPoint = new Uri("/site/sha", UriKind.Relative);
|
||||
readonly ProductHeaderValue productHeader;
|
||||
readonly IHttpClient httpClient;
|
||||
|
||||
[ImportingConstructor]
|
||||
public EnterpriseProbe(IProgram program, IHttpClient httpClient)
|
||||
{
|
||||
productHeader = program.ProductHeader;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public IObservable<EnterpriseProbeResult> Probe(Uri enterpriseBaseUrl)
|
||||
{
|
||||
var request = new Request
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
BaseAddress = enterpriseBaseUrl,
|
||||
Endpoint = endPoint,
|
||||
Timeout = TimeSpan.FromSeconds(3),
|
||||
AllowAutoRedirect = false,
|
||||
};
|
||||
request.Headers.Add("User-Agent", productHeader.ToString());
|
||||
|
||||
return httpClient.Send<object>(request)
|
||||
.ToObservable()
|
||||
.Catch(Observable.Return<IResponse<object>>(null))
|
||||
.Select(resp => resp == null
|
||||
? EnterpriseProbeResult.Failed
|
||||
: (resp.StatusCode == HttpStatusCode.OK
|
||||
? EnterpriseProbeResult.Ok
|
||||
: EnterpriseProbeResult.NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
public enum EnterpriseProbeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Yep! It's an Enterprise server
|
||||
/// </summary>
|
||||
Ok,
|
||||
|
||||
/// <summary>
|
||||
/// Got a response from a server, but it wasn't an Enterprise server
|
||||
/// </summary>
|
||||
NotFound,
|
||||
|
||||
/// <summary>
|
||||
/// Request timed out or DNS failed. So it's probably the case it's not an enterprise server but
|
||||
/// we can't know for sure.
|
||||
/// </summary>
|
||||
Failed
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using System.Globalization;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Akavache;
|
||||
using GitHub.Extensions.Reactive;
|
||||
using NLog;
|
||||
using Octokit;
|
||||
using LogManager = NLog.LogManager;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public class GitHubCredentialStore : ICredentialStore
|
||||
{
|
||||
static readonly Logger log = LogManager.GetCurrentClassLogger();
|
||||
readonly HostAddress hostAddress;
|
||||
readonly ILoginCache loginCache;
|
||||
|
||||
public GitHubCredentialStore(HostAddress hostAddress, ILoginCache loginCache)
|
||||
{
|
||||
this.hostAddress = hostAddress;
|
||||
this.loginCache = loginCache;
|
||||
}
|
||||
|
||||
public async Task<Credentials> GetCredentials()
|
||||
{
|
||||
return await loginCache.GetLoginAsync(hostAddress)
|
||||
.CatchNonCritical(Observable.Return(LoginCache.EmptyLoginInfo))
|
||||
.Select(CreateFromLoginInfo);
|
||||
}
|
||||
|
||||
Credentials CreateFromLoginInfo(LoginInfo loginInfo)
|
||||
{
|
||||
if (loginInfo == LoginCache.EmptyLoginInfo)
|
||||
{
|
||||
log.Debug(CultureInfo.InvariantCulture, "Could not retrieve login info from the secure cache '{0}'",
|
||||
hostAddress.CredentialCacheKeyHost);
|
||||
return Credentials.Anonymous;
|
||||
}
|
||||
|
||||
return new Credentials(loginInfo.UserName, loginInfo.Password);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using GitHub.Models;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IAccountFactory
|
||||
{
|
||||
IAccount CreateAccount(
|
||||
IRepositoryHost repositoryHost,
|
||||
Octokit.User user);
|
||||
|
||||
IAccount CreateAccount(
|
||||
IRepositoryHost repositoryHost,
|
||||
Octokit.Organization organization);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using GitHub.Authentication;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IApiClient
|
||||
{
|
||||
HostAddress HostAddress { get; }
|
||||
IObservable<SshKey> AddSshKey(SshKey newKey);
|
||||
IObservable<Repository> CreateRepository(Repository repo, string login, bool isUser);
|
||||
IObservable<SshKey> GetSshKeys();
|
||||
IObservable<User> GetUser();
|
||||
IObservable<User> GetAllUsersForAllOrganizations();
|
||||
IObservable<Organization> GetOrganization(string login);
|
||||
IObservable<Organization> GetOrganizations();
|
||||
IObservable<User> GetMembersOfOrganization(string organizationName);
|
||||
IObservable<Repository> GetRepository(string owner, string name);
|
||||
IObservable<IEnumerable<Repository>> GetUserRepositories(int currentUserId);
|
||||
IObservable<Repository> GetCurrentUserRepositoriesStreamed();
|
||||
IObservable<Repository> GetOrganizationRepositoriesStreamed(string login);
|
||||
IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
|
||||
Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander = null,
|
||||
bool useOldScopes = false);
|
||||
IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
|
||||
string authenticationCode,
|
||||
bool useOldScopes = false);
|
||||
IObservable<IReadOnlyList<EmailAddress>> GetEmails();
|
||||
ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace GitHub
|
||||
{
|
||||
public interface IApiClientFactory
|
||||
{
|
||||
IApiClient Create(HostAddress hostAddress);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace GitHub.Services
|
||||
{
|
||||
public interface IBrowser
|
||||
{
|
||||
void OpenUrl(Uri url);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using GitHub.Helpers;
|
||||
|
||||
namespace GitHub.Services
|
||||
{
|
||||
public interface IEnterpriseProbe
|
||||
{
|
||||
IObservable<EnterpriseProbeResult> Probe(Uri enterpriseBaseUrl);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using GitHub.Models;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public interface IRepositoryHostFactory
|
||||
{
|
||||
IRepositoryHost Create(HostAddress hostAddress);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using GitHub.Models;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
[Export(typeof(IRepositoryHostFactory))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class RepositoryHostFactory : IRepositoryHostFactory
|
||||
{
|
||||
readonly IApiClientFactory apiClientFactory;
|
||||
readonly IHostCacheFactory hostCacheFactory;
|
||||
readonly ILoginCache loginCache;
|
||||
readonly IAccountFactory accountFactory;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryHostFactory(
|
||||
IApiClientFactory apiClientFactory,
|
||||
IHostCacheFactory hostCacheFactory,
|
||||
ILoginCache loginCache,
|
||||
IAccountFactory accountFactory)
|
||||
{
|
||||
this.apiClientFactory = apiClientFactory;
|
||||
this.hostCacheFactory = hostCacheFactory;
|
||||
this.loginCache = loginCache;
|
||||
this.accountFactory = accountFactory;
|
||||
}
|
||||
|
||||
public IRepositoryHost Create(HostAddress hostAddress)
|
||||
{
|
||||
var apiClient = apiClientFactory.Create(hostAddress);
|
||||
var hostCache = hostCacheFactory.Create(hostAddress);
|
||||
|
||||
return new RepositoryHost(apiClient, hostCache, loginCache, accountFactory);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Services
|
||||
{
|
||||
public enum ErrorType
|
||||
{
|
||||
BranchCreateFailed,
|
||||
BranchDeleteFailed,
|
||||
BranchCheckoutFailed,
|
||||
BranchPublishFailed,
|
||||
BranchUnpublishFailed,
|
||||
BranchListFailed,
|
||||
CannotDropFolder,
|
||||
ClipboardFailed,
|
||||
ClonedFailed,
|
||||
CloneFailedNotLoggedIn,
|
||||
CommitCreateFailed,
|
||||
CommitRevertFailed,
|
||||
CommitUndoFailed,
|
||||
CommitFilesLoadFailed,
|
||||
DeleteEnterpriseServerFailed,
|
||||
DiscardFileChangesFailed,
|
||||
DiscardAllChangesFailed,
|
||||
IgnoreFileFailed,
|
||||
EnterpriseConnectFailed,
|
||||
GitExtractionFailed,
|
||||
GettingHeadFailed,
|
||||
LaunchEnterpriseConnectionFailed,
|
||||
LoginFailed,
|
||||
LogFileError,
|
||||
RepoCorrupted,
|
||||
RepoDirectoryAlreadyExists,
|
||||
RepoCreationFailed,
|
||||
RepoCreationOnGitHubFailed,
|
||||
RepoCreationAsPrivateNotAvailableForFreePlan,
|
||||
RepoExistsOnDisk,
|
||||
RepoExistsForUser,
|
||||
RepoExistsInOrganization,
|
||||
RepositoryNotFoundOnDisk,
|
||||
SyncFailed,
|
||||
ShellFailed,
|
||||
CustomShellFailed,
|
||||
WorkingDirectoryDoesNotExist,
|
||||
MergeFailed,
|
||||
PowerShellNotFound,
|
||||
LoadingCommitsFailed,
|
||||
LoadingWorkingDirectoryFailed,
|
||||
SaveRepositorySettingsFailed,
|
||||
MenuActionFailed,
|
||||
Global,
|
||||
RefreshFailed
|
||||
}
|
||||
|
||||
public static class StandardUserErrors
|
||||
{
|
||||
public static IObservable<RecoveryOptionResult> ShowUserErrorMessage(
|
||||
this Exception ex, ErrorType errorType, params object[] messageArgs)
|
||||
{
|
||||
// TODO: Fix this. This is just placeholder logic. -@haacked
|
||||
Console.WriteLine(errorType);
|
||||
Console.WriteLine(messageArgs);
|
||||
MessageBox.Show(ex.Message);
|
||||
return Observable.Return(new RecoveryOptionResult());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,440 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Helpers;
|
||||
using GitHub.Info;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using GitHub.Validation;
|
||||
using NullGuard;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels
|
||||
{
|
||||
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
|
||||
[Export(typeof(LoginControlViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class LoginControlViewModel : ReactiveValidatableObject, IDisposable
|
||||
{
|
||||
string enterpriseUrl;
|
||||
readonly ObservableAsPropertyHelper<bool> isLoggingInToEnterprise;
|
||||
readonly ObservableAsPropertyHelper<bool> isLoginInProgress;
|
||||
readonly Subject<AuthenticationResult> authenticationResults;
|
||||
readonly ObservableAsPropertyHelper<string> loginButtonText;
|
||||
bool loginFailed;
|
||||
string loginFailedText;
|
||||
readonly ObservableAsPropertyHelper<LoginMode> loginMode;
|
||||
readonly ObservableAsPropertyHelper<LoginTarget> loginTarget;
|
||||
readonly ObservableAsPropertyHelper<VisualState> visualState;
|
||||
string password;
|
||||
string usernameOrEmail;
|
||||
Uri enterpriseHostBaseUrl;
|
||||
readonly Lazy<IEnterpriseProbe> lazyEnterpriseProbe;
|
||||
const string notEnterpriseServerError = "Not an Enterprise server. Please enter an Enterprise URL";
|
||||
readonly ObservableAsPropertyHelper<Uri> forgotPasswordUrl;
|
||||
|
||||
[SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "It's Rx baby")]
|
||||
[ImportingConstructor]
|
||||
public LoginControlViewModel(
|
||||
IServiceProvider serviceProvider,
|
||||
IRepositoryHosts repositoryHosts,
|
||||
IBrowser browser,
|
||||
Lazy<IEnterpriseProbe> enterpriseProbe) : base(serviceProvider)
|
||||
{
|
||||
RepositoryHosts = repositoryHosts;
|
||||
lazyEnterpriseProbe = enterpriseProbe;
|
||||
|
||||
var canLogin = this.WhenAny(x => x.IsValid, x => x.Value);
|
||||
|
||||
this.WhenAny(x => x.LoginFailed, x => x.Value ? "Try again" : "Log in")
|
||||
.ToProperty(this, x => x.LoginButtonText, out loginButtonText);
|
||||
|
||||
LoginPrefix = "Log in";
|
||||
LoginFailedText = "Log in failed";
|
||||
|
||||
LoginCommand = ReactiveCommand.CreateAsyncObservable(canLogin,
|
||||
x => Validate()
|
||||
? LogIn()
|
||||
: Observable.Return(AuthenticationResult.CredentialFailure));
|
||||
|
||||
LoginCommand.IsExecuting
|
||||
.ToProperty(this, vm => vm.IsLoginInProgress, out isLoginInProgress);
|
||||
|
||||
CancelCommand = ReactiveCommand.Create(Observable.Return(true));
|
||||
|
||||
SignupCommand = ReactiveCommand.Create(Observable.Return(true));
|
||||
SignupCommand.Subscribe(_ => browser.OpenUrl(GitHubUrls.Plans));
|
||||
|
||||
LearnMoreCommand = ReactiveCommand.Create(Observable.Return(true));
|
||||
LearnMoreCommand.Subscribe(_ => browser.OpenUrl(GitHubUrls.GitHubEnterpriseWeb));
|
||||
|
||||
// Whenever a host logs on or off we re-evaluate this. If there are no logged on hosts (local excluded)
|
||||
// then the user may log on to either .com or an enterprise instance. If there's already a logged on host
|
||||
// for .com then the user may only log on to an enterprise instance and vice versa.
|
||||
loginMode = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.RepositoryHosts.GitHubHost.IsLoggedIn, x => x.Value),
|
||||
this.WhenAny(x => x.RepositoryHosts.EnterpriseHost.IsLoggedIn, x => x.Value),
|
||||
GetLoginModeFromLoggedInRemoteHosts)
|
||||
.ToProperty(this, x => x.LoginMode);
|
||||
|
||||
var canSwitchTargets = this.WhenAny(
|
||||
x => x.IsLoginInProgress,
|
||||
y => y.LoginMode,
|
||||
(x, y) => new { IsLoginInProgress = x.Value, LoginMode = y.Value }
|
||||
)
|
||||
.Select(x => !x.IsLoginInProgress && x.LoginMode == LoginMode.DotComOrEnterprise);
|
||||
|
||||
ShowDotComLoginCommand = ReactiveCommand.Create(canSwitchTargets);
|
||||
|
||||
ShowEnterpriseLoginCommand = ReactiveCommand.Create(canSwitchTargets);
|
||||
|
||||
Observable.Merge(
|
||||
this.WhenAny(x => x.LoginMode, x => x.Value),
|
||||
ShowEnterpriseLoginCommand.Select(_ => LoginMode.EnterpriseOnly),
|
||||
ShowDotComLoginCommand.Select(_ => LoginMode.DotComOnly),
|
||||
CancelCommand.Select(_ => LoginMode))
|
||||
.Where(x =>
|
||||
x == LoginMode.DotComOnly
|
||||
|| x == LoginMode.EnterpriseOnly
|
||||
|| x == LoginMode.DotComOrEnterprise)
|
||||
.Select(x => x == LoginMode.EnterpriseOnly ? LoginTarget.Enterprise : LoginTarget.DotCom)
|
||||
.ToProperty(this, x => x.LoginTarget, out loginTarget);
|
||||
|
||||
Observable.Merge(
|
||||
ShowDotComLoginCommand,
|
||||
ShowEnterpriseLoginCommand)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
UsernameOrEmail = "";
|
||||
Password = "";
|
||||
ResetValidation();
|
||||
});
|
||||
|
||||
this.WhenAny(
|
||||
x => x.LoginMode,
|
||||
x => x.LoginTarget,
|
||||
(x, y) => new {Mode = x.Value, Target = y.Value})
|
||||
.Select(x => GetStateNameFromModeAndTarget(x.Mode, x.Target))
|
||||
.ToProperty(this, x => x.VisualState, out visualState);
|
||||
|
||||
this.WhenAny(
|
||||
x => x.LoginTarget,
|
||||
x => x.Value == LoginTarget.Enterprise)
|
||||
.ToProperty(this, x => x.IsLoggingInToEnterprise, out isLoggingInToEnterprise);
|
||||
|
||||
authenticationResults = new Subject<AuthenticationResult>();
|
||||
|
||||
this.WhenAny(
|
||||
x => x.EnterpriseHostBaseUrl,
|
||||
x => x.LoginTarget,
|
||||
(x, y) => new { enterpriseBaseUrl = x.Value, loginTarget = y.Value })
|
||||
.Select(x => GetForgotPasswordUrl(x.enterpriseBaseUrl, x.loginTarget))
|
||||
.ToProperty(this, x => x.ForgotPasswordUrl, out forgotPasswordUrl);
|
||||
|
||||
ForgotPasswordCommand = ReactiveCommand.Create();
|
||||
ForgotPasswordCommand.Subscribe(_ => browser.OpenUrl(ForgotPasswordUrl));
|
||||
}
|
||||
|
||||
public ReactiveCommand<object> CancelCommand { get; private set; }
|
||||
|
||||
[ValidateIf("IsLoggingInToEnterprise")]
|
||||
[Required(ErrorMessage = "Please enter an Enterprise URL")]
|
||||
[AllowNull]
|
||||
public string EnterpriseUrl
|
||||
{
|
||||
get { return enterpriseUrl; }
|
||||
set { this.RaiseAndSetIfChanged(ref enterpriseUrl, value); }
|
||||
}
|
||||
|
||||
public ReactiveCommand<object> ForgotPasswordCommand { get; private set; }
|
||||
|
||||
public bool IsLoggingInToEnterprise
|
||||
{
|
||||
get { return isLoggingInToEnterprise.Value; }
|
||||
}
|
||||
|
||||
public bool IsLoginInProgress
|
||||
{
|
||||
get { return isLoginInProgress.Value; }
|
||||
}
|
||||
|
||||
public IObservable<AuthenticationResult> AuthenticationResults { get { return authenticationResults; } }
|
||||
|
||||
public string LoginButtonText
|
||||
{
|
||||
get { return loginButtonText.Value; }
|
||||
}
|
||||
|
||||
public ReactiveCommand<AuthenticationResult> LoginCommand { get; private set; }
|
||||
|
||||
public bool LoginFailed
|
||||
{
|
||||
get { return loginFailed; }
|
||||
set { this.RaiseAndSetIfChanged(ref loginFailed, value); }
|
||||
}
|
||||
|
||||
public string LoginFailedText
|
||||
{
|
||||
get { return loginFailedText; }
|
||||
private set { this.RaiseAndSetIfChanged(ref loginFailedText, value); }
|
||||
}
|
||||
|
||||
public LoginMode LoginMode
|
||||
{
|
||||
get { return loginMode.Value; }
|
||||
}
|
||||
|
||||
public string LoginPrefix { get; set; }
|
||||
|
||||
public LoginTarget LoginTarget
|
||||
{
|
||||
get { return loginTarget.Value; }
|
||||
}
|
||||
|
||||
public VisualState VisualState
|
||||
{
|
||||
get { return visualState.Value; }
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public string Password
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return password; }
|
||||
set { this.RaiseAndSetIfChanged(ref password, value); }
|
||||
}
|
||||
|
||||
public Uri ForgotPasswordUrl
|
||||
{
|
||||
get { return forgotPasswordUrl.Value; }
|
||||
}
|
||||
|
||||
Uri EnterpriseHostBaseUrl
|
||||
{
|
||||
get { return enterpriseHostBaseUrl; }
|
||||
set { this.RaiseAndSetIfChanged(ref enterpriseHostBaseUrl, value); }
|
||||
}
|
||||
|
||||
// HACKETY HACK!
|
||||
// Because #Bind() doesn't yet set up validation, we must use XAML bindings for username and password.
|
||||
// But, because our SecurePasswordBox manipulates base.Text, it doesn't work with XAML binding.
|
||||
// (It binds the password mask, not the password.)
|
||||
// So, this property is a "black hole" to point the XAML binding to so validation works.
|
||||
// And the actual password is bound to #Password via #Bind(). Ugly? Yep.
|
||||
[Required(ErrorMessage = "Please enter your password")]
|
||||
public string PasswordNoOp { get; set; }
|
||||
|
||||
protected IRepositoryHosts RepositoryHosts { get; private set; }
|
||||
|
||||
public ReactiveCommand<object> ShowDotComLoginCommand { get; set; }
|
||||
|
||||
public ReactiveCommand<object> ShowEnterpriseLoginCommand { get; set; }
|
||||
|
||||
public ReactiveCommand<object> SignupCommand { get; private set; }
|
||||
|
||||
public ReactiveCommand<object> LearnMoreCommand { get; private set; }
|
||||
|
||||
[Required(ErrorMessage = "Please enter your username or email address")]
|
||||
[AllowNull]
|
||||
public string UsernameOrEmail
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return usernameOrEmail; }
|
||||
set { this.RaiseAndSetIfChanged(ref usernameOrEmail, value); }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
authenticationResults.OnCompleted();
|
||||
authenticationResults.Dispose();
|
||||
}
|
||||
|
||||
protected IObservable<AuthenticationResult> DoEnterpriseLogIn()
|
||||
{
|
||||
Uri uri;
|
||||
if (EnterpriseUrl.IsNullOrEmpty() || !Uri.TryCreate(EnterpriseUrl, UriKind.Absolute, out uri))
|
||||
{
|
||||
LoginFailed = true;
|
||||
SetErrorMessage("EnterpriseUrl", "Please enter a valid Enterprise URL");
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
var enterpriseHostAddress = HostAddress.Create(uri);
|
||||
|
||||
if (enterpriseHostAddress == HostAddress.GitHubDotComHostAddress)
|
||||
{
|
||||
LoginFailed = true;
|
||||
SetErrorMessage("EnterpriseUrl", notEnterpriseServerError);
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
var enterpriseProbe = lazyEnterpriseProbe.Value;
|
||||
// Make a test request to /site/sha to make sure the provided URL points to a GitHub site:
|
||||
return enterpriseProbe.Probe(enterpriseHostAddress.WebUri)
|
||||
.SelectMany(result => HandleEnterpriseProbeResult(result, enterpriseHostAddress));
|
||||
}
|
||||
|
||||
IObservable<AuthenticationResult> HandleEnterpriseProbeResult(
|
||||
EnterpriseProbeResult result,
|
||||
HostAddress enterpriseHostAddress)
|
||||
{
|
||||
LoginFailed = result != EnterpriseProbeResult.Ok;
|
||||
|
||||
// Something went wrong with the test request, like it doesn't exist or timed out:
|
||||
if (result == EnterpriseProbeResult.Failed)
|
||||
{
|
||||
SetErrorMessage("EnterpriseUrl", "Failed to connect to the URL.");
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
// The test request didn't fail, but it didn't return 200, so it's not a GitHub site:
|
||||
if (result == EnterpriseProbeResult.NotFound)
|
||||
{
|
||||
SetErrorMessage("EnterpriseUrl", notEnterpriseServerError);
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
EnterpriseHostBaseUrl = enterpriseHostAddress.WebUri;
|
||||
|
||||
var host = RepositoryHosts.RepositoryHostFactory.Create(enterpriseHostAddress);
|
||||
|
||||
return DoLogIn(host)
|
||||
.Do(authenticationResult =>
|
||||
{
|
||||
if (authenticationResult.IsSuccess())
|
||||
RepositoryHosts.EnterpriseHost = host;
|
||||
});
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
protected IObservable<AuthenticationResult> DoLogIn(IRepositoryHost host)
|
||||
{
|
||||
LoginFailed = false;
|
||||
if (usernameOrEmail.IsNullOrEmpty() || password.IsNullOrEmpty())
|
||||
{
|
||||
LoginFailed = true;
|
||||
Password = "";
|
||||
return Observable.Return(AuthenticationResult.CredentialFailure);
|
||||
}
|
||||
|
||||
return host.LogIn(usernameOrEmail, password)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Catch<AuthenticationResult, Exception>(ex =>
|
||||
{
|
||||
LoginFailed = true;
|
||||
return Observable.Throw<AuthenticationResult>(ex);
|
||||
})
|
||||
.Do(result =>
|
||||
{
|
||||
authenticationResults.OnNext(result);
|
||||
|
||||
if (result.IsSuccess())
|
||||
return;
|
||||
|
||||
LoginFailed = true;
|
||||
LoginFailedText = String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} failed",
|
||||
(result == AuthenticationResult.CredentialFailure ?
|
||||
LoginPrefix :
|
||||
"Two-factor authentication"));
|
||||
|
||||
})
|
||||
.Finally(() =>
|
||||
{
|
||||
Password = "";
|
||||
ResetValidation();
|
||||
})
|
||||
.Multicast(new AsyncSubject<AuthenticationResult>()).RefCount();
|
||||
}
|
||||
|
||||
private static LoginMode GetLoginModeFromLoggedInRemoteHosts(
|
||||
bool gitHubHostLoggedIn,
|
||||
bool enterpriseHostLoggedIn)
|
||||
{
|
||||
return gitHubHostLoggedIn
|
||||
? enterpriseHostLoggedIn
|
||||
? LoginMode.None
|
||||
: LoginMode.EnterpriseOnly
|
||||
: enterpriseHostLoggedIn // And Not logged into GitHub
|
||||
? LoginMode.DotComOnly
|
||||
: LoginMode.DotComOrEnterprise;
|
||||
}
|
||||
|
||||
public virtual IObservable<AuthenticationResult> LogIn()
|
||||
{
|
||||
return LoginTarget == LoginTarget.Enterprise
|
||||
? DoEnterpriseLogIn()
|
||||
: DoLogIn(RepositoryHosts.GitHubHost);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
LoginPrefix = "Log in";
|
||||
LoginFailed = false;
|
||||
EnterpriseUrl = "";
|
||||
UsernameOrEmail = "";
|
||||
Password = "";
|
||||
ResetValidation();
|
||||
}
|
||||
|
||||
static Uri GetForgotPasswordUrl(Uri validEnterpriseHostBaseUrl, LoginTarget currentLoginTarget)
|
||||
{
|
||||
var baseUrl = currentLoginTarget == LoginTarget.DotCom || validEnterpriseHostBaseUrl == null
|
||||
? HostAddress.GitHubDotComHostAddress.WebUri
|
||||
: validEnterpriseHostBaseUrl;
|
||||
return new Uri(baseUrl, GitHubUrls.ForgotPasswordPath);
|
||||
}
|
||||
|
||||
private static VisualState GetStateNameFromModeAndTarget(LoginMode loginMode, LoginTarget loginTarget)
|
||||
{
|
||||
if (loginMode == LoginMode.DotComOnly || loginMode == LoginMode.EnterpriseOnly)
|
||||
return (VisualState)loginMode;
|
||||
|
||||
switch (loginTarget)
|
||||
{
|
||||
case LoginTarget.DotCom:
|
||||
case LoginTarget.Enterprise:
|
||||
return (VisualState)loginTarget;
|
||||
}
|
||||
|
||||
return VisualState.DotCom;
|
||||
}
|
||||
}
|
||||
|
||||
public enum LoginMode
|
||||
{
|
||||
None = 0,
|
||||
DotComOrEnterprise,
|
||||
DotComOnly = 3,
|
||||
EnterpriseOnly = 4,
|
||||
}
|
||||
|
||||
public enum LoginTarget
|
||||
{
|
||||
None = 0,
|
||||
DotCom = 1,
|
||||
Enterprise = 2,
|
||||
}
|
||||
|
||||
public enum VisualState
|
||||
{
|
||||
None = 0,
|
||||
DotCom = 1,
|
||||
Enterprise = 2,
|
||||
DotComOnly = 3,
|
||||
EnterpriseOnly = 4
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reactive.Linq;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Services;
|
||||
using GitHub.Validation;
|
||||
using NullGuard;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels
|
||||
{
|
||||
[Export(typeof(TwoFactorDialogViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class TwoFactorDialogViewModel : ReactiveValidatableObject
|
||||
{
|
||||
bool isAuthenticationCodeSent;
|
||||
string authenticationCode;
|
||||
TwoFactorType twoFactorType;
|
||||
readonly ObservableAsPropertyHelper<string> description;
|
||||
readonly ObservableAsPropertyHelper<bool> isShowing;
|
||||
readonly ObservableAsPropertyHelper<bool> isSms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public TwoFactorDialogViewModel(IBrowser browser, IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
OkCommand = ReactiveCommand.Create(this.WhenAny(
|
||||
x => x.IsValid,
|
||||
x => x.AuthenticationCode,
|
||||
(valid, y) => valid.Value && (String.IsNullOrEmpty(y.Value) || (y.Value != null && y.Value.Length == 6))));
|
||||
CancelCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
ShowHelpCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
//ShowHelpCommand.Subscribe(x => browser.OpenUrl(twoFactorHelpUri));
|
||||
ResendCodeCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
|
||||
description = this.WhenAny(x => x.TwoFactorType, x => x.Value)
|
||||
.Select(type =>
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorType.Sms:
|
||||
return "We sent you a message via SMS with your authentication code.";
|
||||
case TwoFactorType.AuthenticatorApp:
|
||||
return "Open the two-factor authentication app on your device to view your " +
|
||||
"authentication code.";
|
||||
case TwoFactorType.Unknown:
|
||||
return "Enter a login authentication code here";
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.ToProperty(this, x => x.Description);
|
||||
|
||||
isShowing = this.WhenAny(x => x.TwoFactorType, x => x.Value)
|
||||
.Select(factorType => factorType != TwoFactorType.None)
|
||||
.ToProperty(this, x => x.IsShowing);
|
||||
|
||||
isSms = this.WhenAny(x => x.TwoFactorType, x => x.Value)
|
||||
.Select(factorType => factorType == TwoFactorType.Sms)
|
||||
.ToProperty(this, x => x.IsSms);
|
||||
}
|
||||
|
||||
public TwoFactorType TwoFactorType
|
||||
{
|
||||
get { return twoFactorType; }
|
||||
private set { this.RaiseAndSetIfChanged(ref twoFactorType, value); }
|
||||
}
|
||||
|
||||
public bool IsShowing
|
||||
{
|
||||
get { return isShowing.Value; }
|
||||
}
|
||||
|
||||
public bool IsSms
|
||||
{
|
||||
get { return isSms.Value; }
|
||||
}
|
||||
|
||||
public bool IsAuthenticationCodeSent
|
||||
{
|
||||
get { return isAuthenticationCodeSent; }
|
||||
private set { this.RaiseAndSetIfChanged(ref isAuthenticationCodeSent, value); }
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return description.Value; }
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Please enter your authentication code")]
|
||||
[RegularExpression(@"\d+", ErrorMessage = "Authentication code must only contain numbers")]
|
||||
[AllowNull]
|
||||
public string AuthenticationCode
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return authenticationCode; }
|
||||
set { this.RaiseAndSetIfChanged(ref authenticationCode, value); }
|
||||
}
|
||||
|
||||
public ReactiveCommand<object> OkCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> CancelCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> ShowHelpCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> ResendCodeCommand { get; private set; }
|
||||
|
||||
public IObservable<RecoveryOptionResult> Show(TwoFactorRequiredUserError error)
|
||||
{
|
||||
TwoFactorType = error.TwoFactorType;
|
||||
var ok = OkCommand
|
||||
.Where(x => Validate())
|
||||
.Select(_ => AuthenticationCode == null
|
||||
? RecoveryOptionResult.CancelOperation
|
||||
: RecoveryOptionResult.RetryOperation)
|
||||
.Do(_ => error.ChallengeResult = AuthenticationCode != null
|
||||
? new TwoFactorChallengeResult(AuthenticationCode)
|
||||
: null);
|
||||
var cancel = CancelCommand.Select(_ => RecoveryOptionResult.CancelOperation);
|
||||
var resend = ResendCodeCommand.Select(_ => RecoveryOptionResult.RetryOperation)
|
||||
.Do(_ => error.ChallengeResult = TwoFactorChallengeResult.RequestResendCode);
|
||||
|
||||
return Observable.Merge(ok, cancel, resend)
|
||||
.Take(1)
|
||||
.Do(_ =>
|
||||
{
|
||||
bool authenticationCodeSent = error.ChallengeResult == TwoFactorChallengeResult.RequestResendCode;
|
||||
if (!authenticationCodeSent)
|
||||
{
|
||||
TwoFactorType = TwoFactorType.None;
|
||||
}
|
||||
IsAuthenticationCodeSent = authenticationCodeSent;
|
||||
})
|
||||
.Finally(() =>
|
||||
{
|
||||
AuthenticationCode = null;
|
||||
//TODO: ResetValidation();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Reactive.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Reactive.Linq" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="akavache" version="4.0.3.2" targetFramework="net45" />
|
||||
<package id="akavache.core" version="4.0.3.2" targetFramework="net45" />
|
||||
<package id="akavache.sqlite3" version="4.0.3.2" targetFramework="net45" />
|
||||
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Newtonsoft.Json" version="6.0.4" targetFramework="net45" />
|
||||
<package id="NLog" version="3.1.0.0" targetFramework="net45" />
|
||||
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Octokit" version="0.4.1" targetFramework="net45" />
|
||||
<package id="Octokit.Reactive" version="0.4.1" targetFramework="net45" />
|
||||
<package id="reactiveui" version="6.0.6" targetFramework="net45" />
|
||||
<package id="reactiveui-core" version="6.0.6" targetFramework="net45" />
|
||||
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Main" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-XAML" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
|
||||
<package id="SQLitePCL.raw_basic" version="0.5.0" targetFramework="net45" />
|
||||
</packages>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{6559E128-8B40-49A5-85A8-05565ED0C7E3}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>GitHub.Extensions.Reactive</RootNamespace>
|
||||
<AssemblyName>GitHub.Extensions.Reactive</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>760f38ad</NuGetPackageImportStamp>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="NullGuard">
|
||||
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Reactive.Core">
|
||||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces">
|
||||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq">
|
||||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="ObservableExtensions.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
|
||||
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
|
||||
<Name>GitHub.Extensions</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace GitHub.Extensions.Reactive
|
||||
{
|
||||
public static class ObservableExtensions
|
||||
{
|
||||
public static IObservable<T> WhereNotNull<T>(this IObservable<T> observable) where T : class
|
||||
{
|
||||
return observable.Where(item => item != null);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
|
||||
Justification = "Rx has your back.")]
|
||||
public static IObservable<T> PublishAsync<T>(this IObservable<T> observable)
|
||||
{
|
||||
var ret = observable.Multicast(new AsyncSubject<T>());
|
||||
ret.Connect();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used in cases where we only care when an observable completes and not what the observed values are.
|
||||
/// This is commonly used by some of our "Refresh" methods.
|
||||
/// </summary>
|
||||
public static IObservable<Unit> AsCompletion<T>(this IObservable<T> observable)
|
||||
{
|
||||
return observable
|
||||
.SelectMany(_ => Observable.Empty<Unit>())
|
||||
.Concat(Observable.Return(Unit.Default));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to transform an IObservable{T} to IObservable{Unit}.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="observable"></param>
|
||||
/// <returns></returns>
|
||||
public static IObservable<Unit> SelectUnit<T>(this IObservable<T> observable)
|
||||
{
|
||||
return observable.Select(_ => Unit.Default);
|
||||
}
|
||||
|
||||
public static IObservable<TSource> CatchNonCritical<TSource>(
|
||||
this IObservable<TSource> first,
|
||||
Func<Exception, IObservable<TSource>> second)
|
||||
{
|
||||
return first.Catch<TSource, Exception>(e =>
|
||||
{
|
||||
if (!e.IsCriticalException())
|
||||
return second(e);
|
||||
|
||||
return Observable.Throw<TSource>(e);
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<TSource> CatchNonCritical<TSource>(
|
||||
this IObservable<TSource> first,
|
||||
IObservable<TSource> second)
|
||||
{
|
||||
return first.CatchNonCritical(e => second);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("GitHub.Extensions.Reactive")]
|
||||
[assembly: AssemblyDescription("Provides useful Rx based extension and utility methods common to the needs of GitHub applications")]
|
||||
[assembly: Guid("73e49b11-0bd0-4984-b9a8-3e7edceb071e")]
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
|
||||
</packages>
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
|
||||
{
|
||||
foreach (var item in source) action(item);
|
||||
}
|
||||
|
||||
public static IEnumerable<TSource> Except<TSource>(
|
||||
this IEnumerable<TSource> enumerable,
|
||||
IEnumerable<TSource> second,
|
||||
Func<TSource, TSource, bool> comparer)
|
||||
{
|
||||
return enumerable.Except(second, new LambdaComparer<TSource>(comparer));
|
||||
}
|
||||
|
||||
class LambdaComparer<T> : IEqualityComparer<T>
|
||||
{
|
||||
private readonly Func<T, T, bool> lambdaComparer;
|
||||
private readonly Func<T, int> lambdaHash;
|
||||
|
||||
public LambdaComparer(Func<T, T, bool> lambdaComparer) :
|
||||
this(lambdaComparer, o => 0)
|
||||
{
|
||||
}
|
||||
|
||||
LambdaComparer(Func<T, T, bool> lambdaComparer, Func<T, int> lambdaHash)
|
||||
{
|
||||
this.lambdaComparer = lambdaComparer;
|
||||
this.lambdaHash = lambdaHash;
|
||||
}
|
||||
|
||||
public bool Equals(T x, T y)
|
||||
{
|
||||
return lambdaComparer(x, y);
|
||||
}
|
||||
|
||||
public int GetHashCode(T obj)
|
||||
{
|
||||
return lambdaHash(obj);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace GitHub.Extensions
|
||||
{
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents exceptions we should never attempt to catch and ignore.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception being thrown.</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsCriticalException(this Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
return exception.IsFatalException()
|
||||
|| exception is AppDomainUnloadedException
|
||||
|| exception is BadImageFormatException
|
||||
|| exception is CannotUnloadAppDomainException
|
||||
|| exception is InvalidProgramException
|
||||
|| exception is NullReferenceException
|
||||
|| exception is ArgumentException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents exceptions we should never attempt to catch and ignore when executing third party plugin code.
|
||||
/// This is not as extensive as a proposed IsCriticalException method that I want to write for our own code.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception being thrown.</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsFatalException(this Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException("exception");
|
||||
}
|
||||
|
||||
return exception is StackOverflowException
|
||||
|| exception is OutOfMemoryException
|
||||
|| exception is ThreadAbortException
|
||||
|| exception is AccessViolationException;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>GitHub.Extensions</RootNamespace>
|
||||
<AssemblyName>GitHub.Extensions</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>d4e00d5e</NuGetPackageImportStamp>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="NullGuard">
|
||||
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="EnumerableExtensions.cs" />
|
||||
<Compile Include="ExceptionExtensions.cs" />
|
||||
<Compile Include="Guard.cs" />
|
||||
<Compile Include="ReflectionExtensions.cs" />
|
||||
<Compile Include="StringExtensions.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UriExtensions.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub.Extensions
|
||||
{
|
||||
public static class Guard
|
||||
{
|
||||
public static void ArgumentNonNegative(int value, string name)
|
||||
{
|
||||
if (value > -1) return;
|
||||
|
||||
var message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must be non-negative", name);
|
||||
#if DEBUG
|
||||
if (!InUnitTestRunner())
|
||||
{
|
||||
Debug.Fail(message);
|
||||
}
|
||||
#endif
|
||||
throw new ArgumentException(message, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks a string argument to ensure it isn't null or empty.
|
||||
/// </summary>
|
||||
/// <param name = "value">The argument value to check.</param>
|
||||
/// <param name = "name">The name of the argument.</param>
|
||||
public static void ArgumentNotEmptyString(string value, string name)
|
||||
{
|
||||
if (value.Length > 0) return;
|
||||
string message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must not be empty", name);
|
||||
#if DEBUG
|
||||
if (!InUnitTestRunner())
|
||||
{
|
||||
Debug.Fail(message);
|
||||
}
|
||||
#endif
|
||||
throw new ArgumentException(message, name);
|
||||
}
|
||||
|
||||
public static void ArgumentInRange(int value, int minValue, string name)
|
||||
{
|
||||
if (value >= minValue) return;
|
||||
string message = String.Format(CultureInfo.InvariantCulture,
|
||||
"The value '{0}' for '{1}' must be greater than or equal to '{2}'",
|
||||
value,
|
||||
name,
|
||||
minValue);
|
||||
#if DEBUG
|
||||
if (!InUnitTestRunner())
|
||||
{
|
||||
Debug.Fail(message);
|
||||
}
|
||||
#endif
|
||||
throw new ArgumentOutOfRangeException(name, message);
|
||||
}
|
||||
|
||||
public static void ArgumentInRange(int value, int minValue, int maxValue, string name)
|
||||
{
|
||||
if (value >= minValue && value <= maxValue) return;
|
||||
string message = String.Format(CultureInfo.InvariantCulture,
|
||||
"The value '{0}' for '{1}' must be greater than or equal to '{2}' and less than or equal to '{3}'",
|
||||
value,
|
||||
name,
|
||||
minValue,
|
||||
maxValue);
|
||||
#if DEBUG
|
||||
if (!InUnitTestRunner())
|
||||
{
|
||||
Debug.Fail(message);
|
||||
}
|
||||
#endif
|
||||
throw new ArgumentOutOfRangeException(name, message);
|
||||
}
|
||||
|
||||
// Borrowed from Splat.
|
||||
static bool InUnitTestRunner()
|
||||
{
|
||||
var testAssemblies = new[] {
|
||||
"CSUNIT",
|
||||
"NUNIT",
|
||||
"XUNIT",
|
||||
"MBUNIT",
|
||||
"PEX.",
|
||||
"NBEHAVE",
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return SearchForAssembly(testAssemblies);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool SearchForAssembly(IEnumerable<string> assemblyList)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Any(x => assemblyList.Any(name => x.FullName.ToUpperInvariant().Contains(name)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("GitHub.Extensions")]
|
||||
[assembly: AssemblyDescription("Provides useful extension and utility methods common to the needs of GitHub applications")]
|
||||
[assembly: Guid("3bf91177-3d16-425d-9c62-50a86cf26298")]
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace GitHub.Extensions
|
||||
{
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
return e.Types.Where(t => t != null);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasInterface(this Type type, Type targetInterface)
|
||||
{
|
||||
if (targetInterface.IsAssignableFrom(type))
|
||||
return true;
|
||||
return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == targetInterface);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NullGuard;
|
||||
|
||||
namespace GitHub.Extensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool Contains(this string s, string expectedSubstring, StringComparison comparison)
|
||||
{
|
||||
return s.IndexOf(expectedSubstring, comparison) > -1;
|
||||
}
|
||||
|
||||
public static bool ContainsAny(this string s, IEnumerable<char> characters)
|
||||
{
|
||||
return s.IndexOfAny(characters.ToArray()) > -1;
|
||||
}
|
||||
|
||||
public static string DebugRepresentation([AllowNull]this string s)
|
||||
{
|
||||
s = s ?? "(null)";
|
||||
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", s);
|
||||
}
|
||||
|
||||
public static bool IsNotNullOrEmpty(this string s)
|
||||
{
|
||||
return !string.IsNullOrEmpty(s);
|
||||
}
|
||||
|
||||
public static bool IsNullOrEmpty([AllowNull]this string s)
|
||||
{
|
||||
return string.IsNullOrEmpty(s);
|
||||
}
|
||||
|
||||
public static string ToNullIfEmpty([AllowNull]this string s)
|
||||
{
|
||||
return s.IsNullOrEmpty() ? null : s;
|
||||
}
|
||||
|
||||
public static bool StartsWith([AllowNull]this string s, char c)
|
||||
{
|
||||
if (String.IsNullOrEmpty(s)) return false;
|
||||
return s.First() == c;
|
||||
}
|
||||
|
||||
public static string RightAfter([AllowNull]this string s, string search)
|
||||
{
|
||||
if (s == null) return null;
|
||||
int lastIndex = s.IndexOf(search, StringComparison.OrdinalIgnoreCase);
|
||||
if (lastIndex < 0)
|
||||
return null;
|
||||
|
||||
return s.Substring(lastIndex + search.Length);
|
||||
}
|
||||
|
||||
public static string RightAfterLast(this string s, string search)
|
||||
{
|
||||
if (s == null) return null;
|
||||
int lastIndex = s.LastIndexOf(search, StringComparison.OrdinalIgnoreCase);
|
||||
if (lastIndex < 0)
|
||||
return null;
|
||||
|
||||
return s.Substring(lastIndex + search.Length);
|
||||
}
|
||||
|
||||
public static string LeftBeforeLast([AllowNull]this string s, string search)
|
||||
{
|
||||
if (s == null) return null;
|
||||
int lastIndex = s.LastIndexOf(search, StringComparison.OrdinalIgnoreCase);
|
||||
if (lastIndex < 0)
|
||||
return null;
|
||||
|
||||
return s.Substring(0, lastIndex);
|
||||
}
|
||||
|
||||
// Returns a file name even if the path is FUBAR.
|
||||
public static string ParseFileName([AllowNull]this string path)
|
||||
{
|
||||
if (path == null) return null;
|
||||
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).RightAfterLast(Path.DirectorySeparatorChar + "");
|
||||
}
|
||||
|
||||
// Returns the parent directory even if the path is FUBAR.
|
||||
public static string ParseParentDirectory([AllowNull]this string path)
|
||||
{
|
||||
if (path == null) return null;
|
||||
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).LeftBeforeLast(Path.DirectorySeparatorChar + "");
|
||||
}
|
||||
|
||||
public static string EnsureStartsWith([AllowNull]this string s, char c)
|
||||
{
|
||||
if (s == null) return null;
|
||||
return c + s.TrimStart(c);
|
||||
}
|
||||
|
||||
// Ensures the string ends with the specified character.
|
||||
public static string EnsureEndsWith([AllowNull]this string s, char c)
|
||||
{
|
||||
if (s == null) return null;
|
||||
return s.TrimEnd(c) + c;
|
||||
}
|
||||
|
||||
public static string NormalizePath([AllowNull]this string path)
|
||||
{
|
||||
if (String.IsNullOrEmpty(path)) return null;
|
||||
|
||||
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
public static string TrimEnd([AllowNull]this string s, string suffix)
|
||||
{
|
||||
if (s == null) return null;
|
||||
if (!s.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||
return s;
|
||||
|
||||
return s.Substring(0, s.Length - suffix.Length);
|
||||
}
|
||||
|
||||
public static string RemoveSurroundingQuotes(this string s)
|
||||
{
|
||||
if (s.Length < 2)
|
||||
return s;
|
||||
|
||||
var quoteCharacters = new[] { '"', '\'' };
|
||||
char firstCharacter = s[0];
|
||||
if (!quoteCharacters.Contains(firstCharacter))
|
||||
return s;
|
||||
|
||||
if (firstCharacter != s[s.Length - 1])
|
||||
return s;
|
||||
|
||||
return s.Substring(1, s.Length - 2);
|
||||
}
|
||||
|
||||
public static Int32 ToInt32(this string s)
|
||||
{
|
||||
Int32 val;
|
||||
return Int32.TryParse(s, out val) ? val : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap a string to the specified length.
|
||||
/// </summary>
|
||||
/// <param name="text">The text string to wrap</param>
|
||||
/// <param name="maxLength">The character length to wrap at</param>
|
||||
/// <returns>A wrapped string using the platform's default newline character. This string will end in a newline.</returns>
|
||||
public static string Wrap(this string text, int maxLength = 72)
|
||||
{
|
||||
if (text.Length == 0) return string.Empty;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var unwrappedLine in text.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
|
||||
{
|
||||
var line = new StringBuilder();
|
||||
foreach (var word in unwrappedLine.Split(' '))
|
||||
{
|
||||
var needsLeadingSpace = line.Length > 0;
|
||||
|
||||
var extraLength = (needsLeadingSpace ? 1 : 0) + word.Length;
|
||||
if (line.Length + extraLength > maxLength)
|
||||
{
|
||||
sb.AppendLine(line.ToString());
|
||||
line.Clear();
|
||||
needsLeadingSpace = false;
|
||||
}
|
||||
|
||||
if (needsLeadingSpace)
|
||||
line.Append(" ");
|
||||
|
||||
line.Append(word);
|
||||
}
|
||||
|
||||
sb.AppendLine(line.ToString());
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static Uri ToUriSafe(this string url)
|
||||
{
|
||||
Uri uri;
|
||||
Uri.TryCreate(url, UriKind.Absolute, out uri);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
|
||||
namespace GitHub.Extensions
|
||||
{
|
||||
public static class UriExtensions
|
||||
{
|
||||
public static bool IsHypertextTransferProtocol(this Uri uri)
|
||||
{
|
||||
return uri.Scheme == "http" || uri.Scheme == "https";
|
||||
}
|
||||
|
||||
public static bool IsSameHost(this Uri uri, Uri compareUri)
|
||||
{
|
||||
return uri.Host.Equals(compareUri.Host, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static Uri WithAbsolutePath(this Uri uri, string absolutePath)
|
||||
{
|
||||
absolutePath = absolutePath.EnsureStartsWith('/');
|
||||
|
||||
return new Uri(uri, new Uri(absolutePath, UriKind.Relative));
|
||||
}
|
||||
|
||||
public static Uri ToHttps(this Uri uri)
|
||||
{
|
||||
if (uri == null
|
||||
|| uri.Scheme != Uri.UriSchemeHttp
|
||||
|| (uri.Port != 80 && uri.Port != -1)
|
||||
|| uri.IsLoopback)
|
||||
return uri;
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Scheme = Uri.UriSchemeHttps;
|
||||
// trick to keep uriBuilder from explicitly appending :80 to the HTTPS URI
|
||||
uriBuilder.Port = -1;
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
public static string ToUpperInvariantString(this Uri uri)
|
||||
{
|
||||
return uri == null ? "" : uri.ToString().ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
|
||||
</packages>
|
|
@ -0,0 +1,11 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="..\Assets\Controls\Validation\ValidationMessage.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
|
@ -0,0 +1,309 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Extensions;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.UI
|
||||
{
|
||||
public class ReactivePropertyValidationResult
|
||||
{
|
||||
public bool IsValid { get; private set; }
|
||||
public ValidationStatus Status { get; private set; }
|
||||
public string Message { get; private set; }
|
||||
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "It is immutable")]
|
||||
public static readonly ReactivePropertyValidationResult Success = new ReactivePropertyValidationResult(ValidationStatus.Valid);
|
||||
|
||||
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "It is immutable")]
|
||||
public static readonly ReactivePropertyValidationResult Unvalidated = new ReactivePropertyValidationResult();
|
||||
|
||||
public ReactivePropertyValidationResult()
|
||||
: this(ValidationStatus.Unvalidated, "")
|
||||
{
|
||||
}
|
||||
|
||||
public ReactivePropertyValidationResult(ValidationStatus validationStatus)
|
||||
: this(validationStatus, "")
|
||||
{
|
||||
}
|
||||
|
||||
public ReactivePropertyValidationResult(ValidationStatus validationStatus, string message)
|
||||
{
|
||||
Status = validationStatus;
|
||||
IsValid = validationStatus != ValidationStatus.Invalid;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ValidationStatus
|
||||
{
|
||||
Unvalidated = 0,
|
||||
Invalid = 1,
|
||||
Valid = 2,
|
||||
}
|
||||
|
||||
public abstract class ReactivePropertyValidator : ReactiveObject
|
||||
{
|
||||
public static ReactivePropertyValidator<TProp> For<TObj, TProp>(TObj This, Expression<Func<TObj, TProp>> property)
|
||||
{
|
||||
return new ReactivePropertyValidator<TObj, TProp>(This, property);
|
||||
}
|
||||
|
||||
public abstract ReactivePropertyValidationResult ValidationResult { get; protected set; }
|
||||
|
||||
public abstract bool IsValidating { get; }
|
||||
|
||||
protected ReactivePropertyValidator()
|
||||
{
|
||||
}
|
||||
|
||||
public abstract Task ExecuteAsync();
|
||||
|
||||
public abstract Task ResetAsync();
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
|
||||
public class ReactivePropertyValidator<TProp> : ReactivePropertyValidator
|
||||
{
|
||||
readonly ReactiveCommand<ReactivePropertyValidationResult> validateCommand;
|
||||
ReactivePropertyValidationResult validationResult;
|
||||
|
||||
public override ReactivePropertyValidationResult ValidationResult
|
||||
{
|
||||
get { return validationResult; }
|
||||
protected set { this.RaiseAndSetIfChanged(ref validationResult, value); }
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync()
|
||||
{
|
||||
return validateCommand.ExecuteAsyncTask(new ValidationParameter());
|
||||
}
|
||||
|
||||
public override Task ResetAsync()
|
||||
{
|
||||
return validateCommand.ExecuteAsyncTask(new ValidationParameter { RequiresReset = true });
|
||||
}
|
||||
|
||||
readonly List<Func<TProp, IObservable<ReactivePropertyValidationResult>>> validators =
|
||||
new List<Func<TProp, IObservable<ReactivePropertyValidationResult>>>();
|
||||
|
||||
readonly ObservableAsPropertyHelper<bool> isValidating;
|
||||
public override bool IsValidating
|
||||
{
|
||||
get { return isValidating.Value; }
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator(IObservable<TProp> signal)
|
||||
{
|
||||
validateCommand = ReactiveCommand.CreateAsyncObservable(param =>
|
||||
{
|
||||
var validationParams = (ValidationParameter)param;
|
||||
|
||||
if (validationParams.RequiresReset)
|
||||
{
|
||||
return Observable.Return(ReactivePropertyValidationResult.Unvalidated);
|
||||
}
|
||||
|
||||
TProp value = validationParams.PropertyValue;
|
||||
|
||||
var currentValidators = validators.ToList();
|
||||
|
||||
// HEAR YE, HEAR YE
|
||||
|
||||
// This .ToList() is here to ignore changes to the validator collection,
|
||||
// and thus avoid fantastically vague exceptions about
|
||||
// "Collection was modified, enumeration operation may not execute"
|
||||
// bubbling up to tear the application down
|
||||
|
||||
// Thus, the collection will be correct when the command executes,
|
||||
// which should be fine until we need to do more complex validation
|
||||
|
||||
if (!currentValidators.Any())
|
||||
return Observable.Return(ReactivePropertyValidationResult.Unvalidated);
|
||||
|
||||
return currentValidators.ToObservable()
|
||||
.SelectMany(v => v(value))
|
||||
.FirstOrDefaultAsync(x => x.Status == ValidationStatus.Invalid)
|
||||
.Select(x => x == null ? ReactivePropertyValidationResult.Success : x);
|
||||
});
|
||||
|
||||
isValidating = validateCommand.IsExecuting.ToProperty(this, x => x.IsValidating);
|
||||
|
||||
validateCommand.Subscribe(x => ValidationResult = x);
|
||||
signal.Subscribe(x => validateCommand.Execute(new ValidationParameter { PropertyValue = x, RequiresReset = false }));
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> IfTrue(Func<TProp, bool> predicate, string errorMessage)
|
||||
{
|
||||
return Add(predicate, errorMessage);
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> IfFalse(Func<TProp, bool> predicate, string errorMessage)
|
||||
{
|
||||
return Add(x => !predicate(x), errorMessage);
|
||||
}
|
||||
|
||||
ReactivePropertyValidator<TProp> Add(Func<TProp, bool> predicate, string errorMessage)
|
||||
{
|
||||
return Add(x => predicate(x) ? errorMessage : null);
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> Add(Func<TProp, string> predicateWithMessage)
|
||||
{
|
||||
validators.Add(value => Observable.Defer(() => Observable.Return(Validate(value, predicateWithMessage))));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> IfTrueAsync(Func<TProp, IObservable<bool>> predicate, string errorMessage)
|
||||
{
|
||||
AddAsync(x => predicate(x).Select(result => result ? errorMessage : null));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> IfFalseAsync(Func<TProp, IObservable<bool>> predicate, string errorMessage)
|
||||
{
|
||||
AddAsync(x => predicate(x).Select(result => result ? null : errorMessage));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<TProp> AddAsync(Func<TProp, IObservable<string>> predicateWithMessage)
|
||||
{
|
||||
validators.Add(value => Observable.Defer(() =>
|
||||
{
|
||||
return predicateWithMessage(value)
|
||||
.Select(result => String.IsNullOrEmpty(result)
|
||||
? ReactivePropertyValidationResult.Success
|
||||
: new ReactivePropertyValidationResult(ValidationStatus.Invalid, result));
|
||||
|
||||
}));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static ReactivePropertyValidationResult Validate(TProp value, Func<TProp, string> predicateWithMessage)
|
||||
{
|
||||
var result = predicateWithMessage(value);
|
||||
|
||||
if (String.IsNullOrEmpty(result))
|
||||
return ReactivePropertyValidationResult.Success;
|
||||
|
||||
return new ReactivePropertyValidationResult(ValidationStatus.Invalid, result);
|
||||
}
|
||||
|
||||
class ValidationParameter
|
||||
{
|
||||
public TProp PropertyValue { get; set; }
|
||||
public bool RequiresReset { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class ReactivePropertyValidator<TObj, TProp> : ReactivePropertyValidator<TProp>
|
||||
{
|
||||
protected ReactivePropertyValidator()
|
||||
: base(Observable.Empty<TProp>())
|
||||
{
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator(TObj This, Expression<Func<TObj, TProp>> property)
|
||||
: base(This.WhenAny(property, x => x.Value)) { }
|
||||
}
|
||||
|
||||
public static class ReactivePropertyValidatorExtensions
|
||||
{
|
||||
public static ReactivePropertyValidator<string> IfMatch(this ReactivePropertyValidator<string> This, string pattern, string errorMessage)
|
||||
{
|
||||
var regex = new Regex(pattern);
|
||||
|
||||
return This.IfTrue(regex.IsMatch, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfNotMatch(this ReactivePropertyValidator<string> This, string pattern, string errorMessage)
|
||||
{
|
||||
var regex = new Regex(pattern);
|
||||
|
||||
return This.IfFalse(regex.IsMatch, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfNullOrEmpty(this ReactivePropertyValidator<string> This, string errorMessage)
|
||||
{
|
||||
return This.IfTrue(String.IsNullOrEmpty, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfNotUri(this ReactivePropertyValidator<string> This, string errorMessage)
|
||||
{
|
||||
return This.IfFalse(s =>
|
||||
{
|
||||
Uri uri;
|
||||
return Uri.TryCreate(s, UriKind.Absolute, out uri);
|
||||
}, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfSameAsHost(this ReactivePropertyValidator<string> This, Uri compareToHost, string errorMessage)
|
||||
{
|
||||
return This.IfTrue(s =>
|
||||
{
|
||||
Uri uri;
|
||||
var isUri = Uri.TryCreate(s, UriKind.Absolute, out uri);
|
||||
return isUri && uri.IsSameHost(compareToHost);
|
||||
|
||||
}, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfContainsInvalidPathChars(this ReactivePropertyValidator<string> This, string errorMessage)
|
||||
{
|
||||
return This.IfTrue(str =>
|
||||
{
|
||||
// easiest check to make
|
||||
if (str.ContainsAny(Path.GetInvalidPathChars()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string driveLetter;
|
||||
|
||||
try
|
||||
{
|
||||
// if for whatever reason you don't have an absolute path
|
||||
// hopefully you've remembered to use `IfPathNotRooted`
|
||||
// in your validator
|
||||
driveLetter = Path.GetPathRoot(str);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Path.GetPathRoot does some fun things
|
||||
// around legal combinations of characters that we miss
|
||||
// by simply checking against an array of legal characters
|
||||
return true;
|
||||
}
|
||||
|
||||
if (driveLetter == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// lastly, check each directory name doesn't contain
|
||||
// any invalid filename characters
|
||||
var foldersInPath = str.Substring(driveLetter.Length);
|
||||
return foldersInPath.Split(new[] { '\\', '/' }, StringSplitOptions.None)
|
||||
.Any(x => x.ContainsAny(Path.GetInvalidFileNameChars()));
|
||||
}, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfPathNotRooted(this ReactivePropertyValidator<string> This, string errorMessage)
|
||||
{
|
||||
return This.IfFalse(Path.IsPathRooted, errorMessage);
|
||||
}
|
||||
|
||||
public static ReactivePropertyValidator<string> IfUncPath(this ReactivePropertyValidator<string> This, string errorMessage)
|
||||
{
|
||||
return This.IfTrue(str => str.StartsWith(@"\\", StringComparison.Ordinal), errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
using GitHub.Extensions.Reactive;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.UI
|
||||
{
|
||||
public class ValidationMessage : UserControl
|
||||
{
|
||||
const double defaultTextChangeThrottle = 0.2;
|
||||
|
||||
public ValidationMessage()
|
||||
{
|
||||
this.WhenAny(x => x.ReactiveValidator.ValidationResult, x => x.Value)
|
||||
.WhereNotNull()
|
||||
.Subscribe(result =>
|
||||
{
|
||||
ShowError = result.IsValid == false;
|
||||
Text = result.Message;
|
||||
});
|
||||
|
||||
this.WhenAny(x => x.ValidatesControl, x => x.Value)
|
||||
.WhereNotNull()
|
||||
.Select(control =>
|
||||
Observable.Merge(
|
||||
control.Events().TextChanged
|
||||
.Throttle(TimeSpan.FromSeconds(ShowError ? defaultTextChangeThrottle : TextChangeThrottle),
|
||||
RxApp.MainThreadScheduler)
|
||||
.Select(_ => ShowError),
|
||||
control.Events().LostFocus
|
||||
.Select(_ => ShowError),
|
||||
control.Events().LostFocus
|
||||
.Where(__ => string.IsNullOrEmpty(ValidatesControl.Text))
|
||||
.Select(_ => false)))
|
||||
.Switch()
|
||||
.Subscribe(showError =>
|
||||
{
|
||||
IsShowingMessage = showError;
|
||||
});
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsShowingMessageProperty = DependencyProperty.Register("IsShowingMessage", typeof(bool), typeof(ValidationMessage));
|
||||
public bool IsShowingMessage
|
||||
{
|
||||
get { return (bool)GetValue(IsShowingMessageProperty); }
|
||||
private set { SetValue(IsShowingMessageProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ValidationMessage));
|
||||
public string Text
|
||||
{
|
||||
get { return (string)GetValue(TextProperty); }
|
||||
private set { SetValue(TextProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ShowErrorProperty = DependencyProperty.Register("ShowError", typeof(bool), typeof(ValidationMessage));
|
||||
public bool ShowError
|
||||
{
|
||||
get { return (bool)GetValue(ShowErrorProperty); }
|
||||
set { SetValue(ShowErrorProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextChangeThrottleProperty = DependencyProperty.Register("TextChangeThrottle", typeof(double), typeof(ValidationMessage), new PropertyMetadata(defaultTextChangeThrottle));
|
||||
public double TextChangeThrottle
|
||||
{
|
||||
get { return (double)GetValue(TextChangeThrottleProperty); }
|
||||
set { SetValue(TextChangeThrottleProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ValidatesControlProperty = DependencyProperty.Register("ValidatesControl", typeof(TextBox), typeof(ValidationMessage), new PropertyMetadata(default(TextBox)));
|
||||
public TextBox ValidatesControl
|
||||
{
|
||||
get { return (TextBox)GetValue(ValidatesControlProperty); }
|
||||
set { SetValue(ValidatesControlProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ReactiveValidatorProperty = DependencyProperty.Register("ReactiveValidator", typeof(ReactivePropertyValidator), typeof(ValidationMessage));
|
||||
public ReactivePropertyValidator ReactiveValidator
|
||||
{
|
||||
get { return (ReactivePropertyValidator)GetValue(ReactiveValidatorProperty); }
|
||||
set { SetValue(ReactiveValidatorProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(Octicon), typeof(ValidationMessage), new PropertyMetadata(Octicon.stop));
|
||||
public Octicon Icon
|
||||
{
|
||||
get { return (Octicon) GetValue(IconProperty); }
|
||||
set { SetValue(IconProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty FillProperty =
|
||||
DependencyProperty.Register("Fill", typeof(Brush), typeof(ValidationMessage), new PropertyMetadata(new SolidColorBrush(Color.FromRgb(0xe7, 0x4c, 0x3c))));
|
||||
public Brush Fill
|
||||
{
|
||||
get { return (Brush)GetValue(FillProperty); }
|
||||
set { SetValue(FillProperty, value); }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:GitHub.UI"
|
||||
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI">
|
||||
|
||||
<Style TargetType="{x:Type local:ValidationMessage}">
|
||||
<Setter Property="Padding" Value="2,6" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:ValidationMessage}">
|
||||
|
||||
<Grid x:Name="grid" Visibility="Collapsed" Height="0" Opacity="0" Margin="{TemplateBinding Padding}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Viewbox Width="16" Height="16" Grid.Column="0"
|
||||
Visibility="{TemplateBinding ShowError, Converter={ui:BooleanToVisibilityConverter}}">
|
||||
<ui:OcticonPath x:Name="icon" Icon="{TemplateBinding Icon}" Height="1024" Fill="{TemplateBinding Fill}"/>
|
||||
</Viewbox>
|
||||
<TextBlock Grid.Column="1" Text="{TemplateBinding Text}"
|
||||
Margin="3,0,0,2"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Resources>
|
||||
<Storyboard x:Key="Show" Storyboard.TargetName="grid">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)">
|
||||
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="20"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="Hide" Storyboard.TargetName="grid">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)">
|
||||
<SplineDoubleKeyFrame KeyTime="0:0:0.2" Value="20"/>
|
||||
<SplineDoubleKeyFrame KeyTime="0:0:0.4" Value="0"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
|
||||
<SplineDoubleKeyFrame KeyTime="0" Value="1"/>
|
||||
<SplineDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="0:0:0.4" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</ControlTemplate.Resources>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsShowingMessage" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard x:Name="Show_BeginStoryboard" Storyboard="{StaticResource Show}"/>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<RemoveStoryboard BeginStoryboardName="Show_BeginStoryboard"/>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
<Trigger Property="IsShowingMessage" Value="False">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard x:Name="Hide_BeginStoryboard" Storyboard="{StaticResource Hide}"/>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<RemoveStoryboard BeginStoryboardName="Hide_BeginStoryboard"/>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
</ControlTemplate.Triggers>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -0,0 +1,144 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>GitHub</RootNamespace>
|
||||
<AssemblyName>GitHub.UI.Reactive</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>2c878333</NuGetPackageImportStamp>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="NullGuard">
|
||||
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="ReactiveUI, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ReactiveUI.Events, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\reactiveui-events.6.0.6\lib\net45\ReactiveUI.Events.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Interactive">
|
||||
<HintPath>..\..\packages\Ix_Experimental-Main.1.1.10823\lib\Net4\System.Interactive.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Core">
|
||||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces">
|
||||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq">
|
||||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.PlatformServices">
|
||||
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Windows.Threading">
|
||||
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Assets\Controls\Validation\ReactivePropertyValidator.cs" />
|
||||
<Compile Include="Assets\Controls\Validation\ValidationMessage.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Validation\ReactiveValidatableObject.cs" />
|
||||
<Compile Include="Validation\ValidateIfAttribute.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Include="Assets\Controls.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Assets\Controls\Validation\ValidationMessage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
|
||||
<Project>{6559E128-8B40-49A5-85A8-05565ED0C7E3}</Project>
|
||||
<Name>GitHub.Extensions.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
|
||||
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
|
||||
<Name>GitHub.Extensions</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
|
||||
<Project>{346384DD-2445-4A28-AF22-B45F3957BD89}</Project>
|
||||
<Name>GitHub.UI</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
|
@ -0,0 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("GitHub.UI.Recative")]
|
||||
[assembly: AssemblyDescription("GitHub flavored WPF styles and controls that require Rx and RxUI")]
|
||||
[assembly: Guid("885a491c-1d13-49e7-baa6-d61f424befcb")]
|
|
@ -0,0 +1,218 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using GitHub.Extensions;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.Validation
|
||||
{
|
||||
public class ReactiveValidatableObject : ReactiveObject, IDataErrorInfo
|
||||
{
|
||||
static readonly ConcurrentDictionary<Type, Dictionary<string, ValidatedProperty>> typeValidatorsMap = new ConcurrentDictionary<Type, Dictionary<string, ValidatedProperty>>();
|
||||
const string isValidPropertyName = "IsValid";
|
||||
readonly IServiceProvider serviceProvider;
|
||||
readonly Dictionary<string, ValidatedProperty> validatedProperties;
|
||||
readonly Dictionary<string, bool> invalidProperties = new Dictionary<string, bool>();
|
||||
readonly Dictionary<string, bool> enabledProperties = new Dictionary<string, bool>();
|
||||
bool validationEnabled = true;
|
||||
|
||||
public ReactiveValidatableObject(IServiceProvider serviceProvider)
|
||||
{
|
||||
validatedProperties = typeValidatorsMap.GetOrAdd(GetType(), GetValidatedProperties);
|
||||
this.serviceProvider = serviceProvider; // This is allowed to be null.
|
||||
|
||||
// NOTE: Until a property has been changed, we don't want to mark it as invalid
|
||||
// as far as IDataErrorInfo is concerned.
|
||||
Changed.Where(x => validatedProperties.ContainsKey(x.PropertyName))
|
||||
.Subscribe(x => enabledProperties[x.PropertyName] = true);
|
||||
}
|
||||
|
||||
public string this[string propertyName]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!validationEnabled || !enabledProperties.ContainsKey(propertyName)) return null;
|
||||
|
||||
string errorMessage = GetErrorMessage(propertyName);
|
||||
bool isValid = errorMessage == null;
|
||||
|
||||
if (isValid && invalidProperties.ContainsKey(propertyName))
|
||||
{
|
||||
invalidProperties.Remove(propertyName);
|
||||
this.RaisePropertyChanged(isValidPropertyName); // TODO: See if I can make this more declarative with a reactive dictionary, if there is one.
|
||||
}
|
||||
else if (!isValid)
|
||||
{
|
||||
invalidProperties[propertyName] = true;
|
||||
this.RaisePropertyChanged(isValidPropertyName); // TODO: See if I can make this more declarative with a reactive dictionary, if there is one.
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Need to ask the author why this is not implemented and to add a message as such to the exception.")]
|
||||
public string Error
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get { return !invalidProperties.Any(); }
|
||||
}
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
bool wasValid = IsValid;
|
||||
var unvalidated = EnableValidationForUnvalidatedProperties();
|
||||
|
||||
unvalidated.ForEach(TriggerValidationForProperty);
|
||||
if (IsValid != wasValid)
|
||||
{
|
||||
this.RaisePropertyChanged(isValidPropertyName);
|
||||
}
|
||||
return IsValid;
|
||||
}
|
||||
|
||||
public void ResetValidation()
|
||||
{
|
||||
try
|
||||
{
|
||||
validationEnabled = false;
|
||||
TriggerValidationForAllProperties(); // Tell the UI every property is now valid.
|
||||
invalidProperties.Clear();
|
||||
enabledProperties.Clear();
|
||||
foreach (var v in validatedProperties.Values)
|
||||
v.Reset();
|
||||
this.RaisePropertyChanged(isValidPropertyName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
validationEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Enables validation for any properties that have not yet been validated and
|
||||
// returns that set of properties.
|
||||
IEnumerable<string> EnableValidationForUnvalidatedProperties()
|
||||
{
|
||||
return validatedProperties.Keys.Where(key => !enabledProperties.ContainsKey(key))
|
||||
.Do(propertyName => enabledProperties[propertyName] = true);
|
||||
}
|
||||
|
||||
// Made this its own method because it's not clear that the way to trigger validation is to
|
||||
// raise a property changed event.
|
||||
void TriggerValidationForProperty(string propertyName)
|
||||
{
|
||||
this.RaisePropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
void TriggerValidationForAllProperties()
|
||||
{
|
||||
validatedProperties.Keys.ForEach(TriggerValidationForProperty);
|
||||
}
|
||||
|
||||
|
||||
string GetErrorMessage(string propertyName)
|
||||
{
|
||||
ValidatedProperty validatedProperty;
|
||||
if (!validatedProperties.TryGetValue(propertyName, out validatedProperty))
|
||||
return null; // TODO: This would be a good place to do default data type validation as the need arises.
|
||||
|
||||
var validationResult = validatedProperty.GetFirstValidationError(this, serviceProvider);
|
||||
return validationResult == ValidationResult.Success ? null : validationResult.ErrorMessage;
|
||||
}
|
||||
|
||||
public void SetErrorMessage(string propertyName, string errorMessage)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(propertyName, "propertyName");
|
||||
Guard.ArgumentNotEmptyString(errorMessage, "errorMessage");
|
||||
|
||||
ValidatedProperty validatedProperty;
|
||||
if (!validatedProperties.TryGetValue(propertyName, out validatedProperty))
|
||||
return;
|
||||
|
||||
validatedProperty.AddValidator(new SetErrorValidator(errorMessage, validatedProperty.Property.GetValue(this, null)));
|
||||
TriggerValidationForProperty(propertyName);
|
||||
}
|
||||
|
||||
static Dictionary<string, ValidatedProperty> GetValidatedProperties(Type type)
|
||||
{
|
||||
return (from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
let validated = new ValidatedProperty(property)
|
||||
where validated.Validators.Any()
|
||||
select validated).ToDictionary(p => p.Property.Name, p => p);
|
||||
}
|
||||
|
||||
class ValidatedProperty
|
||||
{
|
||||
readonly IList<ValidationAttribute> validators;
|
||||
|
||||
public ValidatedProperty(PropertyInfo property)
|
||||
{
|
||||
Property = property;
|
||||
validators = property.GetCustomAttributes(typeof(ValidationAttribute), true).Cast<ValidationAttribute>().ToList();
|
||||
Validators = validators.Where(v => !(v is ValidateIfAttribute));
|
||||
ConditionalValidation = validators.FirstOrDefault(v => v is ValidateIfAttribute) as ValidateIfAttribute;
|
||||
}
|
||||
|
||||
public void AddValidator(ValidationAttribute validator)
|
||||
{
|
||||
validators.Add(validator);
|
||||
}
|
||||
|
||||
public ValidationResult GetFirstValidationError(object instance, IServiceProvider serviceProvider)
|
||||
{
|
||||
var validationContext = new ValidationContext(instance, serviceProvider, null) { MemberName = Property.Name };
|
||||
|
||||
if (ConditionalValidation != null && !ConditionalValidation.IsValidationRequired(validationContext))
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
var value = Property.GetValue(instance, null);
|
||||
return (from validator in Validators
|
||||
let r = validator.GetValidationResult(value, validationContext)
|
||||
where r != null
|
||||
select r).FirstOrDefault();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
var setErrorValidators = validators.Where(x => x is SetErrorValidator).ToList();
|
||||
setErrorValidators.ForEach(x => validators.Remove(x));
|
||||
}
|
||||
|
||||
public PropertyInfo Property { get; private set; }
|
||||
public IEnumerable<ValidationAttribute> Validators { get; private set; }
|
||||
ValidateIfAttribute ConditionalValidation { get; set; }
|
||||
}
|
||||
|
||||
sealed class SetErrorValidator : ValidationAttribute
|
||||
{
|
||||
readonly string errorMessage;
|
||||
readonly object originalValue;
|
||||
|
||||
public SetErrorValidator(string errorMessage, object originalValue)
|
||||
{
|
||||
this.errorMessage = errorMessage;
|
||||
this.originalValue = originalValue;
|
||||
}
|
||||
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
if (originalValue.Equals(value))
|
||||
return new ValidationResult(errorMessage);
|
||||
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using GitHub.Extensions;
|
||||
using NullGuard;
|
||||
|
||||
namespace GitHub.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to conditionally run a set of validators. If this attribute is applied, and
|
||||
/// the dependent property is false, no other validators run. This special case logic
|
||||
/// occurs in ReactiveValidatableObject.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class ValidateIfAttribute : ValidationAttribute
|
||||
{
|
||||
public ValidateIfAttribute(string dependentPropertyName)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(dependentPropertyName, "dependentPropertyName");
|
||||
|
||||
DependentPropertyName = dependentPropertyName;
|
||||
}
|
||||
|
||||
protected override ValidationResult IsValid([AllowNull]object value, [AllowNull]ValidationContext validationContext)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
public bool IsValidationRequired(ValidationContext validationContext)
|
||||
{
|
||||
var instance = validationContext.ObjectInstance;
|
||||
Debug.Assert(instance != null, "The ValidationContext does not allow null instances.");
|
||||
var property = instance.GetType().GetProperty(DependentPropertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (property == null || property.PropertyType != typeof(bool))
|
||||
{
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Could not find a boolean property named '{0}", DependentPropertyName));
|
||||
}
|
||||
|
||||
return (bool)property.GetValue(instance, null);
|
||||
}
|
||||
|
||||
public string DependentPropertyName { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Ix_Experimental-Main" version="1.1.10823" targetFramework="net45" />
|
||||
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="reactiveui" version="6.0.6" targetFramework="net45" />
|
||||
<package id="reactiveui-core" version="6.0.6" targetFramework="net45" />
|
||||
<package id="reactiveui-events" version="6.0.6" targetFramework="net45" />
|
||||
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-Main" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Rx-XAML" version="2.2.5" targetFramework="net45" />
|
||||
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
|
||||
</packages>
|
|
@ -1,4 +1,8 @@
|
|||
using System.Windows;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace GitHub.UI
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
using System.Windows;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace GitHub.UI
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -11,7 +11,11 @@
|
|||
<AssemblyName>GitHub.UI</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<NuGetPackageImportStamp>6cab0329</NuGetPackageImportStamp>
|
||||
<NuGetPackageImportStamp>1145ea6a</NuGetPackageImportStamp>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
@ -20,10 +24,6 @@
|
|||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
|
||||
<AssemblyOriginatorKeyFile>..\..\build\Key.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
|
@ -58,6 +58,9 @@
|
|||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Controls\Octicons\Octicon.cs" />
|
||||
<Compile Include="Controls\Octicons\OcticonPath.cs" />
|
||||
<Compile Include="Controls\Octicons\OcticonPaths.Designer.cs" />
|
||||
|
@ -128,7 +131,6 @@
|
|||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Key.snk" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -140,12 +142,12 @@
|
|||
<EmbeddedResource Include="Controls\Octicons\OcticonPaths.resx" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets')" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets'))" />
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace GitHub.UI
|
||||
|
@ -60,10 +57,7 @@ namespace GitHub.UI
|
|||
|
||||
public static SolidColorBrush CreateBrush(string color)
|
||||
{
|
||||
var convertedColor = ColorConverter.ConvertFromString(color);
|
||||
Debug.Assert(convertedColor != null,
|
||||
String.Format(CultureInfo.InvariantCulture, "The converted Color '{0}' should not be null", color));
|
||||
return CreateBrush((Color)convertedColor);
|
||||
return CreateBrush((Color)ColorConverter.ConvertFromString(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("GitHub.UI")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("GitHub.UI")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2014")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: AssemblyDescription("GitHub flavored WPF styles and controls")]
|
||||
[assembly: Guid("f3cec21e-6a86-43ae-97a6-a274fa31efbe")]
|
||||
|
||||
[assembly: NeutralResourcesLanguage("en-US")]
|
||||
|
||||
[assembly: AssemblyVersion("0.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.1.0.0")]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Fody" version="1.26.3-beta0001" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
|
||||
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
|
||||
</packages>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Weavers>
|
||||
<NullGuard />
|
||||
</Weavers>
|
|
@ -1,47 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
|
||||
<PropertyGroup>
|
||||
<MinimumVisualStudioVersion>12.0</MinimumVisualStudioVersion>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<OldToolsVersion>12.0</OldToolsVersion>
|
||||
<UpgradeBackupLocation />
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<UpdateEnabled>false</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||
<UpdatePeriodically>false</UpdatePeriodically>
|
||||
<UpdateRequired>false</UpdateRequired>
|
||||
<MapFileExtensions>true</MapFileExtensions>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
<NuGetPackageImportStamp>39465276</NuGetPackageImportStamp>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<ProjectGuid>{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}</ProjectGuid>
|
||||
<ProjectGuid>{11569514-5AE5-4B5B-92A2-F10B0967DE5F}</ProjectGuid>
|
||||
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>GitHub.VisualStudio</RootNamespace>
|
||||
<AssemblyName>GitHub.VisualStudio</AssemblyName>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>
|
||||
</AssemblyOriginatorKeyFile>
|
||||
<StartAction>Program</StartAction>
|
||||
<StartProgram>$(DevEnvDir)\devenv.exe</StartProgram>
|
||||
<StartArguments>/rootsuffix Exp</StartArguments>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<GeneratePkgDefFile>false</GeneratePkgDefFile>
|
||||
<IncludeAssemblyInVSIXContainer>false</IncludeAssemblyInVSIXContainer>
|
||||
<IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
|
||||
<IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
|
||||
<CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
|
||||
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
@ -51,8 +33,9 @@
|
|||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
|
@ -61,49 +44,239 @@
|
|||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Reference Include="EditorUtils, Version=1.4.0.0, Culture=neutral, PublicKeyToken=3d1514c4742e0252, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\EditorUtils.1.4.0.0\lib\net40\EditorUtils.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Language.Intellisense, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Text.Data, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Text.Logic, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Text.UI, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Text.UI.Wpf, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.OLE.Interop" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop.8.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop.9.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop.10.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop.11.0">
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Interop.12.0">
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TextManager.Interop" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.12.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.11.0" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.12.0" />
|
||||
<Reference Include="NullGuard">
|
||||
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="ReactiveUI, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Design" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq">
|
||||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Windows.Threading, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="System.Xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<COMReference Include="EnvDTE100">
|
||||
<Guid>{26AD1324-4B7C-44BC-84F8-B86AED45729F}</Guid>
|
||||
<VersionMajor>10</VersionMajor>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>primary</WrapperTool>
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
<COMReference Include="EnvDTE90">
|
||||
<Guid>{2CE2370E-D744-4936-A090-3FFFE667B0E1}</Guid>
|
||||
<VersionMajor>9</VersionMajor>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>primary</WrapperTool>
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
<COMReference Include="Microsoft.VisualStudio.CommandBars">
|
||||
<Guid>{1CBA492E-7263-47BB-87FE-639000619B15}</Guid>
|
||||
<VersionMajor>8</VersionMajor>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>primary</WrapperTool>
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
<COMReference Include="stdole">
|
||||
<Guid>{00020430-0000-0000-C000-000000000046}</Guid>
|
||||
<VersionMajor>2</VersionMajor>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<Lcid>0</Lcid>
|
||||
<WrapperTool>primary</WrapperTool>
|
||||
<Isolated>False</Isolated>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\build\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Guids.cs" />
|
||||
<Compile Include="Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="GlobalSuppressions.cs" />
|
||||
<Compile Include="GitHubPackage.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="PkgCmdID.cs" />
|
||||
<Compile Include="UI\DrawingExtensions.cs" />
|
||||
<Compile Include="UI\Views\Controls\LoginControl.xaml.cs">
|
||||
<DependentUpon>LoginControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\Views\CreateIssueDialog.xaml.cs">
|
||||
<DependentUpon>CreateIssueDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\Views\LoginCommandDialog.xaml.cs">
|
||||
<DependentUpon>LoginCommandDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\Views\TwoFactorView.xaml.cs">
|
||||
<DependentUpon>TwoFactorView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="VSPackage.resx">
|
||||
<MergeWithCTO>true</MergeWithCTO>
|
||||
<ManifestResourceName>VSPackage</ManifestResourceName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="source.extension.vsixmanifest">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.5">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>Microsoft .NET Framework 4.5 %28x86 and x64%29</ProductName>
|
||||
<Install>true</Install>
|
||||
</BootstrapperPackage>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
<VSCTCompile Include="GitHub.VisualStudio.vsct">
|
||||
<ResourceName>Menus.ctmenu</ResourceName>
|
||||
</VSCTCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Resources\Images.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="FodyWeavers.xml" />
|
||||
<CodeAnalysisDictionary Include="..\..\build\CodeAnalysisDictionary.xml">
|
||||
<Link>Properties\CodeAnalysisDictionary.xml</Link>
|
||||
</CodeAnalysisDictionary>
|
||||
<Content Include="Resources\Package.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<IncludeInVSIX>true</IncludeInVSIX>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Include="UI\Views\Controls\LoginControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="UI\Views\CreateIssueDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="UI\Views\LoginCommandDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="UI\Views\TwoFactorView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GitHub.App\GitHub.App.csproj">
|
||||
<Project>{1a1da411-8d1f-4578-80a6-04576bea2dc5}</Project>
|
||||
<Name>GitHub.App</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
|
||||
<Project>{6559e128-8b40-49a5-85a8-05565ed0c7e3}</Project>
|
||||
<Name>GitHub.Extensions.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
|
||||
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
|
||||
<Name>GitHub.Extensions</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj">
|
||||
<Project>{158b05e8-fdbc-4d71-b871-c96e28d5adf5}</Project>
|
||||
<Name>GitHub.UI.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
|
||||
<Project>{346384dd-2445-4a28-af22-b45f3957bd89}</Project>
|
||||
<Name>GitHub.UI</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
|
||||
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
|
||||
<Name>Rothko</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<UseCodebase>true</UseCodebase>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
|
||||
</Target>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<CommandTable
|
||||
xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
|
||||
<!-- This is the file that defines the actual layout and type of the commands.
|
||||
It is divided in different sections (e.g. command definition, command
|
||||
placement, ...), with each defining a specific set of properties.
|
||||
See the comment before each section for more details about how to
|
||||
use it. -->
|
||||
|
||||
<!-- The VSCT compiler (the tool that translates this file into the binary
|
||||
format that VisualStudio will consume) has the ability to run a preprocessor
|
||||
on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
|
||||
it is possible to define includes and macros with the same syntax used
|
||||
in C++ files. Using this ability of the compiler here, we include some files
|
||||
defining some of the constants that we will use inside the file. -->
|
||||
|
||||
<!--This is the file that defines the IDs for all the commands exposed by VisualStudio. -->
|
||||
<Extern href="stdidcmd.h"/>
|
||||
|
||||
<!--This header contains the command ids for the menus provided by the shell. -->
|
||||
<Extern href="vsshlids.h"/>
|
||||
|
||||
<!--The Commands section is where we the commands, menus and menu groups are defined.
|
||||
This section uses a Guid to identify the package that provides the command defined inside it. -->
|
||||
<Commands package="guidGitHubPkg">
|
||||
<Menus>
|
||||
<Menu guid="guidGitHubCmdSet" id="GitHubMenu" priority="0x700" type="Menu">
|
||||
<Parent guid="guidSHLMainMenu" id="IDG_VS_MM_TOOLSADDINS" />
|
||||
<Strings>
|
||||
<ButtonText>&GitHub</ButtonText>
|
||||
</Strings>
|
||||
</Menu>
|
||||
</Menus>
|
||||
|
||||
<Groups>
|
||||
<Group guid="guidGitHubCmdSet" id="GitHubMenuGroup" priority="0x0600">
|
||||
<Parent guid="guidGitHubCmdSet" id="GitHubMenu" />
|
||||
</Group>
|
||||
</Groups>
|
||||
|
||||
<!--Buttons section. -->
|
||||
<!--This section defines the elements the user can interact with, like a menu command or a button
|
||||
or combo box in a toolbar. -->
|
||||
<Buttons>
|
||||
<!--To define a menu group you have to specify its ID, the parent menu and its display priority.
|
||||
The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
|
||||
the CommandFlag node.
|
||||
You can add more than one CommandFlag node e.g.:
|
||||
<CommandFlag>DefaultInvisible</CommandFlag>
|
||||
<CommandFlag>DynamicVisibility</CommandFlag>
|
||||
If you do not want an image next to your command, remove the Icon node /> -->
|
||||
<Button guid="guidGitHubCmdSet" id="loginCommand" priority="0x0100" type="Button">
|
||||
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
|
||||
<Icon guid="guidImages" id="bmpPic1" />
|
||||
<Strings>
|
||||
<ButtonText>&Login</ButtonText>
|
||||
</Strings>
|
||||
</Button>
|
||||
<Button guid="guidGitHubCmdSet" id="createIssueCommand" priority="0x0101" type="Button">
|
||||
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
|
||||
<Icon guid="guidImages" id="bmpPic1" />
|
||||
<Strings>
|
||||
<ButtonText>&Create Issue</ButtonText>
|
||||
</Strings>
|
||||
</Button>
|
||||
<Button guid="guidGitHubCmdSet" id="goToIssueCommand" priority="0x0102" type="Button">
|
||||
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
|
||||
<Icon guid="guidImages" id="bmpPic1" />
|
||||
<Strings>
|
||||
<ButtonText>&Go to issue</ButtonText>
|
||||
</Strings>
|
||||
</Button>
|
||||
<Button guid="guidGitHubCmdSet" id="viewIssuesCommand" priority="0x0103" type="Button">
|
||||
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
|
||||
<Icon guid="guidImages" id="bmpPic1" />
|
||||
<Strings>
|
||||
<ButtonText>&View Issues</ButtonText>
|
||||
</Strings>
|
||||
</Button>
|
||||
</Buttons>
|
||||
|
||||
<!--The bitmaps section is used to define the bitmaps that are used for the commands.-->
|
||||
<Bitmaps>
|
||||
<!-- The bitmap id is defined in a way that is a little bit different from the others:
|
||||
the declaration starts with a guid for the bitmap strip, then there is the resource id of the
|
||||
bitmap strip containing the bitmaps and then there are the numeric ids of the elements used
|
||||
inside a button definition. An important aspect of this declaration is that the element id
|
||||
must be the actual index (1-based) of the bitmap inside the bitmap strip. -->
|
||||
<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>
|
||||
</Bitmaps>
|
||||
</Commands>
|
||||
|
||||
<KeyBindings>
|
||||
<KeyBinding guid="guidGitHubCmdSet" id="loginCommand" editor="guidVSStd97" key1="U" key2="L" mod2="CONTROL" mod1="CONTROL"/>
|
||||
<KeyBinding guid="guidGitHubCmdSet" id="createIssueCommand" editor="guidVSStd97" key1="U" key2="C" mod2="CONTROL" mod1="CONTROL"/>
|
||||
<KeyBinding guid="guidGitHubCmdSet" id="goToIssueCommand" editor="guidVSStd97" key1="U" key2="G" mod2="CONTROL" mod1="CONTROL"/>
|
||||
<KeyBinding guid="guidGitHubCmdSet" id="viewIssuesCommand" editor="guidVSStd97" key1="U" key2="V" mod2="CONTROL" mod1="CONTROL"/>
|
||||
</KeyBindings>
|
||||
|
||||
<Symbols>
|
||||
<!-- This is the package guid. -->
|
||||
<GuidSymbol name="guidGitHubPkg" value="{c3d3dc68-c977-411f-b3e8-03b0dccf7dfc}" />
|
||||
|
||||
<!-- This is the guid used to group the menu commands together -->
|
||||
<GuidSymbol name="guidGitHubCmdSet" value="{c4c91892-8881-4588-a5d9-b41e8f540f5a}">
|
||||
<IDSymbol name="GitHubMenuGroup" value="0x1020" />
|
||||
<IDSymbol name="loginCommand" value="0x0110" />
|
||||
<IDSymbol name="createIssueCommand" value="0x0100" />
|
||||
<IDSymbol name="goToIssueCommand" value="0x0101" />
|
||||
<IDSymbol name="viewIssuesCommand" value="0x0102" />
|
||||
<IDSymbol name="GitHubMenu" value="0x1021"/>
|
||||
</GuidSymbol>
|
||||
|
||||
<GuidSymbol name="guidImages" value="{27841f47-070a-46d6-90be-a5cbbfc724ac}" >
|
||||
<IDSymbol name="bmpPic1" value="1" />
|
||||
<IDSymbol name="bmpPic2" value="2" />
|
||||
<IDSymbol name="bmpPicSearch" value="3" />
|
||||
<IDSymbol name="bmpPicX" value="4" />
|
||||
<IDSymbol name="bmpPicArrows" value="5" />
|
||||
<IDSymbol name="bmpPicStrikethrough" value="6" />
|
||||
</GuidSymbol>
|
||||
</Symbols>
|
||||
|
||||
</CommandTable>
|
|
@ -0,0 +1,173 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.Composition.Hosting;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Infrastructure;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.VisualStudio.UI.Views;
|
||||
using Microsoft.VisualStudio.ComponentModelHost;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using ReactiveUI;
|
||||
using Splat;
|
||||
|
||||
namespace GitHub.VisualStudio
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the class that implements the package exposed by this assembly.
|
||||
///
|
||||
/// The minimum requirement for a class to be considered a valid package for Visual Studio
|
||||
/// is to implement the IVsPackage interface and register itself with the shell.
|
||||
/// This package uses the helper classes defined inside the Managed Package Framework (MPF)
|
||||
/// to do it: it derives from the Package class that provides the implementation of the
|
||||
/// IVsPackage interface and uses the registration attributes defined in the framework to
|
||||
/// register itself and its components with the shell.
|
||||
/// </summary>
|
||||
// This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is
|
||||
// a package.
|
||||
[PackageRegistration(UseManagedResourcesOnly = true)]
|
||||
// This attribute is used to register the information needed to show this package
|
||||
// in the Help/About dialog of Visual Studio.
|
||||
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
|
||||
// This attribute is needed to let the shell know that this package exposes some menus.
|
||||
[ProvideMenuResource("Menus.ctmenu", 1)]
|
||||
[Guid(GuidList.guidGitHubPkgString)]
|
||||
[ProvideBindingPath]
|
||||
public class GitHubPackage : Package
|
||||
{
|
||||
readonly IServiceProvider serviceProvider;
|
||||
|
||||
// Set of assemblies we need to load early.
|
||||
static readonly IEnumerable<string> earlyLoadAssemblies = new[] {
|
||||
"Rothko.dll",
|
||||
"GitHub.App.dll",
|
||||
"GitHub.UI.Reactive.dll",
|
||||
"GitHub.UI.dll"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor of the package.
|
||||
/// Inside this method you can place any initialization code that does not require
|
||||
/// any Visual Studio service because at this point the package object is created but
|
||||
/// not sited yet inside Visual Studio environment. The place to do all the other
|
||||
/// initialization is the Initialize method.
|
||||
/// </summary>
|
||||
public GitHubPackage()
|
||||
{
|
||||
serviceProvider = this;
|
||||
}
|
||||
|
||||
public GitHubPackage(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialization of the package; this method is called right after the package is sited, so this is the place
|
||||
/// where you can put all the initialization code that rely on services provided by VisualStudio.
|
||||
/// </summary>
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFile")]
|
||||
protected override void Initialize()
|
||||
{
|
||||
Debug.WriteLine("Entering Initialize() of: {0}", ToString());
|
||||
base.Initialize();
|
||||
|
||||
ModeDetector.OverrideModeDetector(new AppModeDetector());
|
||||
RxApp.MainThreadScheduler = new DispatcherScheduler(Application.Current.Dispatcher);
|
||||
|
||||
var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
Debug.Assert(dir != null, "The Assembly location can't be null");
|
||||
foreach (var v in earlyLoadAssemblies)
|
||||
{
|
||||
Assembly.LoadFile(Path.Combine(dir, v));
|
||||
}
|
||||
|
||||
// Set the Export Provider
|
||||
var mefServiceProvider = GetExportedValue<IServiceProvider>() as MefServiceProvider;
|
||||
Debug.Assert(mefServiceProvider != null, "Service Provider can't be imported");
|
||||
var componentModel = (IComponentModel)(serviceProvider.GetService(typeof(SComponentModel)));
|
||||
mefServiceProvider.ExportProvider = componentModel.DefaultExportProvider;
|
||||
|
||||
// Add our command handlers for menu (commands must exist in the .vsct file)
|
||||
var mcs = serviceProvider.GetService(typeof(IMenuCommandService)) as IMenuCommandService;
|
||||
if (mcs != null)
|
||||
{
|
||||
// Login Command Menu Item
|
||||
AddTopLevelMenuItem(mcs, PkgCmdIDList.loginCommand, OnLoginCommand);
|
||||
|
||||
// Create Issue Command Menu Item
|
||||
AddTopLevelMenuItem(mcs, PkgCmdIDList.createIssueCommand, OnCreateIssueCommand);
|
||||
}
|
||||
}
|
||||
|
||||
static void AddTopLevelMenuItem(
|
||||
IMenuCommandService menuCommandService,
|
||||
uint packageCommandId,
|
||||
EventHandler eventHandler)
|
||||
{
|
||||
var menuCommandId = new CommandID(GuidList.guidGitHubCmdSet, (int)packageCommandId);
|
||||
var menuItem = new MenuCommand(eventHandler, menuCommandId);
|
||||
menuCommandService.AddCommand(menuItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is the callback used to execute a command when the a menu item is clicked.
|
||||
/// See the Initialize method to see how the menu item is associated to this function using
|
||||
/// the OleMenuCommandService service and the MenuCommand class.
|
||||
/// </summary>
|
||||
static void OnCreateIssueCommand(object sender, EventArgs e)
|
||||
{
|
||||
var createIssueDialog = new CreateIssueDialog();
|
||||
createIssueDialog.ShowModal();
|
||||
}
|
||||
|
||||
void OnLoginCommand(object sender, EventArgs e)
|
||||
{
|
||||
var loginControlViewModel = GetExportedValue<LoginControlViewModel>();
|
||||
|
||||
var loginIssueDialog = new LoginCommandDialog(loginControlViewModel);
|
||||
loginIssueDialog.Show();
|
||||
loginControlViewModel.AuthenticationResults.Subscribe(result =>
|
||||
{
|
||||
if (result == AuthenticationResult.Success)
|
||||
loginIssueDialog.Hide();
|
||||
});
|
||||
}
|
||||
|
||||
T GetExportedValue<T>()
|
||||
{
|
||||
var componentModel = (IComponentModel)(serviceProvider.GetService(typeof(SComponentModel)));
|
||||
var exportProvider = componentModel.DefaultExportProvider;
|
||||
return exportProvider.GetExportedValue<T>();
|
||||
}
|
||||
}
|
||||
|
||||
[Export(typeof(IServiceProvider))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class MefServiceProvider : IServiceProvider
|
||||
{
|
||||
public ExportProvider ExportProvider { get; set; }
|
||||
|
||||
public object GetService(Type serviceType)
|
||||
{
|
||||
string contract = AttributedModelServices.GetContractName(serviceType);
|
||||
var instance = ExportProvider.GetExportedValues<object>(contract).FirstOrDefault();
|
||||
|
||||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
|
||||
"Could not locate any instances of contract {0}.", contract));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project. Project-level
|
||||
// suppressions either have no target or are given a specific target
|
||||
// and scoped to a namespace, type, member, etc.
|
||||
//
|
||||
// To add a suppression to this file, right-click the message in the
|
||||
// Error List, point to "Suppress Message(s)", and click "In Project
|
||||
// Suppression File". You do not need to add suppressions to this
|
||||
// file manually.
|
||||
|
||||
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1701:ResourceStringCompoundWordsShouldBeCasedCorrectly", MessageId = "GitHub", Scope = "resource", Target = "VSPackage.resources")]
|
|
@ -0,0 +1,14 @@
|
|||
// Guids.cs
|
||||
// MUST match guids.h
|
||||
using System;
|
||||
|
||||
namespace GitHub.VisualStudio
|
||||
{
|
||||
static class GuidList
|
||||
{
|
||||
public const string guidGitHubPkgString = "c3d3dc68-c977-411f-b3e8-03b0dccf7dfc";
|
||||
public const string guidGitHubCmdSetString = "c4c91892-8881-4588-a5d9-b41e8f540f5a";
|
||||
|
||||
public static readonly Guid guidGitHubCmdSet = new Guid(guidGitHubCmdSetString);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// PkgCmdID.cs
|
||||
// MUST match PkgCmdID.h
|
||||
namespace GitHub.VisualStudio
|
||||
{
|
||||
static class PkgCmdIDList
|
||||
{
|
||||
public const uint loginCommand = 0x110;
|
||||
public const uint createIssueCommand = 0x100;
|
||||
};
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("GitHub.VisualStudio")]
|
||||
[assembly: AssemblyDescription("The GitHub for Visual Studio VSIX package")]
|
||||
[assembly: AssemblyDescription("GitHub for Visual Studio VSPackage")]
|
||||
[assembly: Guid("fad77eaa-3fe1-4c4b-88dc-3753b6263cd7")]
|
|
@ -0,0 +1,63 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.34014
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace GitHub.VisualStudio {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GitHub.VisualStudio.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 994 B |
|
@ -0,0 +1,13 @@
|
|||
using System.Windows;
|
||||
|
||||
namespace GitHub.VisualStudio.UI
|
||||
{
|
||||
public static class DrawingExtensions
|
||||
{
|
||||
public static T FreezeThis<T>(this T freezable) where T : Freezable
|
||||
{
|
||||
freezable.Freeze();
|
||||
return freezable;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
<UserControl x:Class="GitHub.VisualStudio.UI.Views.Controls.LoginControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI"
|
||||
xmlns:helpers="clr-namespace:GitHub.Helpers;assembly=GitHub.UI"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="300">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Styles.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Controls.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Buttons.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/TextBlocks.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.UI.Reactive;component/Assets/Controls.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid
|
||||
FocusManager.IsFocusScope="True"
|
||||
x:Name="loginStackPanel"
|
||||
FocusVisualStyle="{x:Null}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" MinHeight="0" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="Normal">
|
||||
<VisualState x:Name="Enterprise">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="dotComTabButton">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="enterpriseTabButton">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False" />
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="dotComTabButton">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<FontWeight>Normal</FontWeight>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="enterpriseTabButton">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<FontWeight>Bold</FontWeight>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="gitHubSignupStackPanel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.MinHeight)" Storyboard.TargetName="fields">
|
||||
<EasingDoubleKeyFrame KeyTime="0" Value="300"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DotCom">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="dotComTabButton">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="False" />
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="enterpriseTabButton">
|
||||
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
|
||||
</BooleanAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="dotComTabButton">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<FontWeight>Bold</FontWeight>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="enterpriseTabButton">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<FontWeight>Normal</FontWeight>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseLearnStackPanel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseUrlTextBox">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.MinHeight)" Storyboard.TargetName="fields">
|
||||
<EasingDoubleKeyFrame KeyTime="0" Value="300"/>
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="EnterpriseOnly">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="tabs">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="gitHubSignupStackPanel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DotComOnly">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="tabs">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseLearnStackPanel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseUrlTextBox">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Design" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<StackPanel x:Name="fields" Margin="24,0,0,12" Background="{x:Null}" FocusManager.IsFocusScope="True" FocusVisualStyle="{x:Null}" helpers:AccessKeysManagerScoping.IsEnabled="True">
|
||||
<StackPanel
|
||||
x:Name="loginLabel"
|
||||
Visibility="{Binding LoginFailed, Mode=OneWay, Converter={ui:BooleanToInverseVisibilityConverter}}"
|
||||
Height="32"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="40,0,0,12">
|
||||
<TextBlock
|
||||
x:Name="loginLabelPrefix"
|
||||
Margin="0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
IsHitTestVisible="False"
|
||||
Text="Log in" />
|
||||
<TextBlock
|
||||
x:Name="dotComOnlyLoginLabel"
|
||||
Margin="0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
IsHitTestVisible="False"
|
||||
Text=" to GitHub" />
|
||||
<TextBlock
|
||||
x:Name="enterpriseOnlyLoginLabel"
|
||||
Margin="0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
IsHitTestVisible="False"
|
||||
Text=" to GitHub Enterprise" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="loginFailed"
|
||||
Margin="40,0,0,12"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{Binding LoginFailed, Converter={ui:BooleanToVisibilityConverter}}"
|
||||
Height="32">
|
||||
<TextBlock x:Name="loginFailedLabel" Margin="0" Style="{DynamicResource GitHubH1TextBlock}" Foreground="#ea2828" Text="{Binding LoginFailedText}" IsHitTestVisible="False" Visibility="{Binding LoginFailed, Converter={ui:BooleanToVisibilityConverter}}" />
|
||||
<ui:OcticonLinkButton x:Name="forgotPasswordButton" Content="Forgot password?" Icon="link_external" Margin="6,3,0,0" IsTabStop="False" Command="{Binding ForgotPasswordCommand}" ToolTip="{Binding ForgotPasswordUrl}" />
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="tabs" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Button
|
||||
x:Name="dotComTabButton"
|
||||
Content="GitHub"
|
||||
IsEnabled="True"
|
||||
Style="{StaticResource GitHubTabButton}" />
|
||||
<Button
|
||||
x:Name="enterpriseTabButton"
|
||||
Content="GitHub Enterprise"
|
||||
IsEnabled="False"
|
||||
Style="{StaticResource GitHubTabButton}" />
|
||||
</StackPanel>
|
||||
<ui:PromptTextBox
|
||||
Margin="0,0,0,12"
|
||||
x:Name="usernameOrEmailTextBox"
|
||||
PromptText="Username or Email"
|
||||
Style="{DynamicResource RoundedPromptTextBox}"
|
||||
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
|
||||
Text="{Binding UsernameOrEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
|
||||
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
|
||||
<ui:SecurePasswordBox
|
||||
x:Name="passwordTextBox"
|
||||
PromptText="Password"
|
||||
Margin="0,0,0,12"
|
||||
Style="{DynamicResource RoundedPromptTextBox}"
|
||||
VerticalAlignment="Stretch"
|
||||
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
|
||||
Text="{Binding PasswordNoOp, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
|
||||
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
|
||||
<ui:PromptTextBox
|
||||
Margin="0"
|
||||
x:Name="enterpriseUrlTextBox"
|
||||
PromptText="GitHub Enterprise URL"
|
||||
Style="{DynamicResource RoundedPromptTextBox}"
|
||||
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
|
||||
Text="{Binding EnterpriseUrl, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
|
||||
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}" Margin="0,12,0,0">
|
||||
<ui:OcticonCircleButton x:Name="loginButton" Icon="check" Content="{Binding LoginButtonText, FallbackValue=Log In}" IsDefault="True" />
|
||||
<ui:OcticonCircleButton x:Name="cancelButton" Icon="x" Margin="12,0,0,0" Content="Cancel" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="gitHubSignupStackPanel">
|
||||
<TextBlock x:Name="signUpLabel" Style="{DynamicResource GitHubH1TextBlock}" Text="Sign up" Margin="0,10,0,0" />
|
||||
<TextBlock x:Name="description" Style="{DynamicResource GitHubDescriptionTextBlock}" Margin="0"><Run Text="Powerful collaboration, review, and code management for open source and private development projects." /><LineBreak /><Run Text="Build software better, together." FontWeight="Bold"/></TextBlock>
|
||||
<ui:OcticonLinkButton x:Name="signUpLink" HorizontalContentAlignment="Right" HorizontalAlignment="Left" Icon="link_external" Content="Sign up" ToolTip="go to github.com to sign up for an account" IsTabStop="False" />
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="enterpriseLearnStackPanel">
|
||||
<TextBlock x:Name="learnMoreLabel" Style="{DynamicResource GitHubH1TextBlock}" Text="Learn more" Margin="0,10,0,0" />
|
||||
<TextBlock Style="{DynamicResource GitHubDescriptionTextBlock}" Margin="0"><Run Text="Powerful collaboration, review, and code management for private development projects." /><LineBreak /><Run Text="The best way to build and ship software, on your servers." FontWeight="Bold" /></TextBlock>
|
||||
<ui:OcticonLinkButton x:Name="learnMoreButton" Icon="link_external" HorizontalContentAlignment="Right" HorizontalAlignment="Left" Content="Learn more" ToolTip="go to enterprise.github.com to learn more about running github on your servers" IsTabStop="False" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче