mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-18 14:24:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d290031f1 | ||
|
|
5220a8c7ac | ||
|
|
33e221f60f | ||
|
|
7f77547c50 | ||
|
|
c5964e33ac | ||
|
|
1c6f3ec797 | ||
|
|
8601f61739 |
@@ -9,6 +9,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "Daybreak/**"
|
||||
- "Daybreak.Installer/**"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -24,7 +27,7 @@ jobs:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreal.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
@@ -57,6 +60,11 @@ jobs:
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v1.0.1
|
||||
|
||||
- name: Setup project secrets
|
||||
run: |
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadTenantId "${{ secrets.AadTenantId }}"
|
||||
|
||||
- name: Restore project
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier
|
||||
env:
|
||||
|
||||
@@ -37,5 +37,7 @@ namespace Daybreak.Configuration
|
||||
public bool PlaceShortcut { get; set; }
|
||||
[JsonProperty("AutoCheckUpdate")]
|
||||
public bool AutoCheckUpdate { get; set; } = true;
|
||||
[JsonProperty("ProtectedGraphAccessToken")]
|
||||
public string ProtectedGraphAccessToken { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ using System.Logging;
|
||||
using Daybreak.Services.Updater.PostUpdate;
|
||||
using System.Core.Extensions;
|
||||
using Daybreak.Services.Updater.PostUpdate.Actions;
|
||||
using Daybreak.Services.Graph;
|
||||
|
||||
namespace Daybreak.Configuration
|
||||
{
|
||||
@@ -79,6 +80,7 @@ namespace Daybreak.Configuration
|
||||
serviceProducer.RegisterScoped<IIconCache, IconCache>();
|
||||
serviceProducer.RegisterScoped<IPrivilegeManager, PrivilegeManager>();
|
||||
serviceProducer.RegisterScoped<IScreenManager, ScreenManager>();
|
||||
serviceProducer.RegisterScoped<IGraphClient, GraphClient>();
|
||||
}
|
||||
|
||||
public static void RegisterViews(IViewProducer viewProducer)
|
||||
@@ -100,6 +102,8 @@ namespace Daybreak.Configuration
|
||||
viewProducer.RegisterView<VersionManagementView>();
|
||||
viewProducer.RegisterView<LogsView>();
|
||||
viewProducer.RegisterView<IconDownloadView>();
|
||||
viewProducer.RegisterView<GraphAuthorizationView>();
|
||||
viewProducer.RegisterView<BuildsSynchronizationView>();
|
||||
}
|
||||
|
||||
public static void RegisterPostUpdateActions(IPostUpdateActionProducer postUpdateActionProducer)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Daybreak.Configuration;
|
||||
|
||||
public class SecretKeys
|
||||
{
|
||||
public static readonly SecretKeys AadApplicationId = new() { Key = "AadApplicationId" };
|
||||
public static readonly SecretKeys AadTenantId = new() { Key = "AadTenantId" };
|
||||
|
||||
public string Key { get; private set; }
|
||||
private SecretKeys()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Extensions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Daybreak.Configuration;
|
||||
|
||||
public static class SecretManager
|
||||
{
|
||||
private static JObject SecretsHolder;
|
||||
|
||||
public static string GetSecret(SecretKeys secretKey)
|
||||
{
|
||||
if (SecretsHolder is null)
|
||||
{
|
||||
LoadSecrets();
|
||||
}
|
||||
|
||||
return SecretsHolder.Value<string>(secretKey.Key);
|
||||
}
|
||||
|
||||
public static T GetSecret<T>(SecretKeys secretKey)
|
||||
{
|
||||
if (SecretsHolder is null)
|
||||
{
|
||||
LoadSecrets();
|
||||
}
|
||||
|
||||
return SecretsHolder.Value<T>(secretKey.Key);
|
||||
}
|
||||
|
||||
private static void LoadSecrets()
|
||||
{
|
||||
var serializedSecrets = Assembly.GetExecutingAssembly().GetManifestResourceStream("Daybreak.secrets.json").ReadAllBytes().GetString();
|
||||
serializedSecrets = TrimUnwantedCharacters(serializedSecrets);
|
||||
SecretsHolder = JObject.Parse(serializedSecrets);
|
||||
}
|
||||
|
||||
private static string TrimUnwantedCharacters(string s)
|
||||
{
|
||||
return new string(s.Where(c =>
|
||||
char.IsWhiteSpace(c) ||
|
||||
char.IsLetterOrDigit(c) ||
|
||||
char.IsSymbol(c) ||
|
||||
char.IsSeparator(c) ||
|
||||
char.IsPunctuation(c))
|
||||
.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<UserControl x:Class="Daybreak.Controls.SynchronizeButton"
|
||||
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:local="clr-namespace:Daybreak.Controls"
|
||||
mc:Ignorable="d"
|
||||
x:Name="_this"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Viewbox>
|
||||
<Grid>
|
||||
<Ellipse x:Name="BackgroundEllipse" Fill="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}" Visibility="Visible" Opacity="0.1" Width="35" Height="35" />
|
||||
<Ellipse Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
StrokeThickness="3"/>
|
||||
<Grid VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<Path Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
StrokeThickness="1.5"
|
||||
Data="m19,15.6073c1.4937,-0.5852 3,-1.9184 3,-4.6073c0,-4 -3.3333,-5 -5,-5c0,-2 0,-6 -6,-6c-6,0 -6,4 -6,6c-1.66667,0 -5,1 -5,5c0,2.6889 1.50628,4.0221 3,4.6073" />
|
||||
<Path StrokeThickness="1.5"
|
||||
Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
Data="m6,17l1.76777,1.7678c1.56214,1.5621 4.09474,1.5621 5.65684,0l0.3536,-0.3536" />
|
||||
<Path StrokeThickness="1.5"
|
||||
Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
Data="m6.35355,19.4749l-0.35355,-2.4749l2.47482,0.3536l-2.12127,2.1213z" />
|
||||
<Path StrokeThickness="1.5"
|
||||
Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
Data="m14.77821,14.93927l-1.7678,-1.7677c-1.5621,-1.5621 -4.0948,-1.5621 -5.65685,0l-0.35356,0.3535" />
|
||||
<Path StrokeThickness="1.5"
|
||||
Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
Data="m14.1213,12l0.3536,2.4749l-2.4749,-0.3536l2.1213,-2.1213z" />
|
||||
</Grid>
|
||||
<Ellipse Fill="Transparent" MouseEnter="Ellipse_MouseEnter" MouseLeave="Ellipse_MouseLeave" MouseLeftButtonDown="Ellipse_MouseLeftButtonDown"></Ellipse>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Daybreak.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for SynchronizeButton.xaml
|
||||
/// </summary>
|
||||
public partial class SynchronizeButton : UserControl
|
||||
{
|
||||
public event EventHandler Clicked;
|
||||
|
||||
public SynchronizeButton()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void Ellipse_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
this.BackgroundEllipse.Opacity = 0.6;
|
||||
}
|
||||
|
||||
private void Ellipse_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
this.BackgroundEllipse.Opacity = 0;
|
||||
}
|
||||
|
||||
private void Ellipse_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left)
|
||||
{
|
||||
this.Clicked?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,16 @@ namespace Daybreak.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitializeBrowser(
|
||||
public async Task InitializeDefaultBrowser()
|
||||
{
|
||||
var options = Launch.Launcher.Instance.ApplicationServiceManager.GetService<ILiveOptions<ApplicationConfiguration>>();
|
||||
var buildTemplateManager = Launch.Launcher.Instance.ApplicationServiceManager.GetService<IBuildTemplateManager>();
|
||||
var logger = Launch.Launcher.Instance.ApplicationServiceManager.GetService<ILogger<ChromiumBrowserWrapper>>();
|
||||
|
||||
await this.InitializeDefaultBrowser(options, buildTemplateManager, logger);
|
||||
}
|
||||
|
||||
public async Task InitializeDefaultBrowser(
|
||||
ILiveOptions<ApplicationConfiguration> liveOptions,
|
||||
IBuildTemplateManager buildTemplateManager,
|
||||
ILogger<ChromiumBrowserWrapper> logger)
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Daybreak.Controls
|
||||
ILogger<ChromiumBrowserWrapper> logger)
|
||||
{
|
||||
this.iconBrowser = iconBrowser.ThrowIfNull();
|
||||
await this.SkillBrowser.InitializeBrowser(liveOptions, buildTemplateManager, logger);
|
||||
await this.SkillBrowser.InitializeDefaultBrowser(liveOptions, buildTemplateManager, logger);
|
||||
this.SkillTemplate0.InitializeSkillTemplate(iconRetriever);
|
||||
this.SkillTemplate1.InitializeSkillTemplate(iconRetriever);
|
||||
this.SkillTemplate2.InitializeSkillTemplate(iconRetriever);
|
||||
|
||||
@@ -10,17 +10,33 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
|
||||
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
|
||||
<Version>0.9.5.2</Version>
|
||||
<Version>0.9.6.2</Version>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="AddUserSecrets" BeforeTargets="PrepareForBuild" Condition=" '$(UserSecretsId)' != '' ">
|
||||
<PropertyGroup>
|
||||
<UserSecretsFilePath Condition=" '$(OS)' == 'Windows_NT' ">
|
||||
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))\AppData\Roaming\Microsoft\UserSecrets\$(UserSecretsId)\secrets.json
|
||||
</UserSecretsFilePath>
|
||||
<UserSecretsFilePath Condition=" '$(OS)' == 'Unix' ">
|
||||
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))/.microsoft/usersecrets/$(UserSecretsId)/secrets.json
|
||||
</UserSecretsFilePath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(UserSecretsFilePath)" Condition="Exists($(UserSecretsFilePath))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteDB" Version="5.0.12" />
|
||||
<PackageReference Include="Microsoft.CorrelationVector" Version="1.0.42" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1293.44" />
|
||||
@@ -39,6 +55,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Controls\Buttons\SynchronizeButton.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Controls\Buttons\HelpButton.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
@@ -57,6 +76,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\Buttons\SynchronizeButton.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Controls\Buttons\HelpButton.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
|
||||
@@ -16,8 +16,11 @@ namespace Daybreak.Launch
|
||||
{
|
||||
public sealed class Launcher : ExtendedApplication<MainWindow>
|
||||
{
|
||||
public readonly static Launcher Instance = new();
|
||||
|
||||
private ILogger logger;
|
||||
private readonly static Launcher launcher = new();
|
||||
|
||||
public IServiceManager ApplicationServiceManager => this.ServiceManager;
|
||||
|
||||
[STAThread]
|
||||
public static int Main()
|
||||
@@ -106,7 +109,7 @@ namespace Daybreak.Launch
|
||||
}
|
||||
private static int LaunchMainWindow()
|
||||
{
|
||||
return launcher.Run();
|
||||
return Instance.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,10 +103,13 @@
|
||||
<Grid x:Name="Container" Grid.Row="1">
|
||||
</Grid>
|
||||
<wcl:Border OnResize="Border_OnResize" Grid.RowSpan="2" Active="True"></wcl:Border>
|
||||
<webview:WebView2 x:Name="BackgroundWebView"
|
||||
<controls:ChromiumBrowserWrapper x:Name="BackgroundWebView"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Collapsed"
|
||||
Grid.Row="1"></webview:WebView2>
|
||||
Visibility="Visible"
|
||||
ControlsEnabled="False"
|
||||
CanNavigate="False"
|
||||
CanDownloadBuild="False"
|
||||
Grid.Row="1"></controls:ChromiumBrowserWrapper>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -80,6 +80,14 @@ namespace Daybreak.Services.BuildTemplates
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearBuilds()
|
||||
{
|
||||
foreach(var file in Directory.GetFiles(BuildsPath))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<BuildEntry> GetBuilds()
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(BuildsPath))
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Daybreak.Services.BuildTemplates
|
||||
bool IsTemplate(string template);
|
||||
BuildEntry CreateBuild();
|
||||
BuildEntry CreateBuild(string name);
|
||||
void ClearBuilds();
|
||||
void SaveBuild(BuildEntry buildEntry);
|
||||
void RemoveBuild(BuildEntry buildEntry);
|
||||
IAsyncEnumerable<BuildEntry> GetBuilds();
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
using Daybreak.Configuration;
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Models.Builds;
|
||||
using Daybreak.Services.BuildTemplates;
|
||||
using Daybreak.Services.Graph.Models;
|
||||
using Daybreak.Services.ViewManagement;
|
||||
using Daybreak.Views;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Daybreak.Services.Graph;
|
||||
|
||||
public sealed class GraphClient : IGraphClient
|
||||
{
|
||||
private const string Scopes = "Files.Read Files.Read.All Files.ReadWrite Files.ReadWrite.All User.Read";
|
||||
private const string RedirectUri = "http://localhost";
|
||||
|
||||
private const string QueryStateKey = "state";
|
||||
private const string QueryCodeKey = "code";
|
||||
private const string ClientIdPlaceholder = "[ClientID]";
|
||||
private const string RedirectUriPlaceholder = "[RedirectUri]";
|
||||
private const string ScopesPlaceholder = "[Scopes]";
|
||||
private const string StatePlaceholder = "[State]";
|
||||
private const string ProfileEndpoint = "me";
|
||||
private const string GraphBaseUrl = "https://graph.microsoft.com/v1.0/";
|
||||
private const string TokenUrlPlaceholder = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||
private const string AuthorizationUrlPlaceholder = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={ClientIdPlaceholder}&response_type=code&redirect_uri={RedirectUriPlaceholder}&response_mode=query&scope={ScopesPlaceholder}&state={StatePlaceholder}";
|
||||
private const string SyncFileUri = $"me/drive/root:/Daybreak/{BackupFileName}";
|
||||
private const string ContentSuffix = ":/content";
|
||||
private const string BackupFileName = "daybreak_backup.json";
|
||||
|
||||
private static readonly byte[] Entropy = Convert.FromBase64String("R3VpbGR3YXJz");
|
||||
private static readonly string ApplicationId = SecretManager.GetSecret(SecretKeys.AadApplicationId);
|
||||
private static readonly string TenantId = SecretManager.GetSecret(SecretKeys.AadTenantId);
|
||||
|
||||
private readonly IBuildTemplateManager buildTemplateManager;
|
||||
private readonly IViewManager viewManager;
|
||||
private readonly ILiveUpdateableOptions<ApplicationConfiguration> liveUpdateableOptions;
|
||||
private readonly IHttpClient<GraphClient> httpClient;
|
||||
private readonly ILogger<GraphClient> logger;
|
||||
|
||||
public GraphClient(
|
||||
IBuildTemplateManager buildTemplateManager,
|
||||
IViewManager viewManager,
|
||||
ILiveUpdateableOptions<ApplicationConfiguration> liveUpdateableOptions,
|
||||
IHttpClient<GraphClient> httpClient,
|
||||
ILogger<GraphClient> logger)
|
||||
{
|
||||
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
|
||||
this.viewManager = viewManager.ThrowIfNull();
|
||||
this.liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull();
|
||||
this.httpClient = httpClient.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
|
||||
this.httpClient.BaseAddress = new Uri(GraphBaseUrl);
|
||||
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async Task<Result<bool, Exception>> PerformAuthorizationFlow(
|
||||
ChromiumBrowserWrapper chromiumBrowserWrapper,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var maybeAuthCode = await this.RetrieveAuthorizationCode(chromiumBrowserWrapper, cancellationToken);
|
||||
if (maybeAuthCode.TryExtractSuccess(out var authCode) is false)
|
||||
{
|
||||
return maybeAuthCode.SwitchAny(onFailure: exception => exception);
|
||||
}
|
||||
|
||||
var maybeAccessToken = await this.RetrieveAccessToken(authCode);
|
||||
|
||||
return maybeAccessToken.Switch(
|
||||
onSuccess: token =>
|
||||
{
|
||||
var accessToken = AccessToken.FromTokenResponse(token);
|
||||
this.SaveAccessToken(accessToken);
|
||||
return true;
|
||||
},
|
||||
onFailure: exception => exception);
|
||||
}
|
||||
|
||||
public async Task<Result<User, Exception>> GetUserProfile<TViewType>()
|
||||
where TViewType : UserControl
|
||||
{
|
||||
var authCode = this.LoadAccessToken();
|
||||
if (authCode.ExtractValue() is not AccessToken accessToken)
|
||||
{
|
||||
this.viewManager.ShowView<GraphAuthorizationView>(new ViewRedirectContext { CallingView = typeof(TViewType) });
|
||||
return new InvalidOperationException("Client is not authorized");
|
||||
}
|
||||
|
||||
if (DateTime.Now > accessToken.ExpirationDate)
|
||||
{
|
||||
this.viewManager.ShowView<GraphAuthorizationView>(new ViewRedirectContext { CallingView = typeof(TViewType) });
|
||||
return new InvalidOperationException("Client authorization expired");
|
||||
}
|
||||
|
||||
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
|
||||
var response = await this.httpClient.GetAsync(ProfileEndpoint);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
this.viewManager.ShowView<GraphAuthorizationView>(new ViewRedirectContext { CallingView = typeof(TViewType) });
|
||||
return new InvalidOperationException($"Failed to load profile. Response status code [{response.StatusCode}]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var profile = JsonConvert.DeserializeObject<User>(await response.Content.ReadAsStringAsync());
|
||||
return profile;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<bool, Exception>> UploadBuilds()
|
||||
{
|
||||
var compiledBuilds = await this.buildTemplateManager.GetBuilds().Select(b => new BuildFile { FileName = b.Name, TemplateCode = this.buildTemplateManager.EncodeTemplate(b.Build) }).ToListAsync();
|
||||
var serializedBuilds = JsonConvert.SerializeObject(compiledBuilds);
|
||||
await this.UploadBackupItem(serializedBuilds);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Result<bool, Exception>> DownloadBuilds()
|
||||
{
|
||||
var maybeBuilds = await this.RetrieveBuildsList();
|
||||
if (maybeBuilds.TryExtractSuccess(out var builds) is false)
|
||||
{
|
||||
return maybeBuilds.SwitchAny(onFailure: exception => exception);
|
||||
}
|
||||
|
||||
this.buildTemplateManager.ClearBuilds();
|
||||
_ = builds.Select(buildFile =>
|
||||
{
|
||||
if (this.buildTemplateManager.TryDecodeTemplate(buildFile.TemplateCode, out var build) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BuildEntry
|
||||
{
|
||||
Build = build,
|
||||
Name = buildFile.FileName,
|
||||
PreviousName = buildFile.FileName
|
||||
};
|
||||
}).Where(entry => entry is not null)
|
||||
.Do(this.buildTemplateManager.SaveBuild).ToList();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Result<List<BuildFile>, Exception>> RetrieveBuildsList()
|
||||
{
|
||||
var maybeCompiledBuilds = await this.GetBackupItemContent();
|
||||
if (maybeCompiledBuilds.ExtractValue() is not string compiledBuilds)
|
||||
{
|
||||
return new InvalidOperationException("No backup found");
|
||||
}
|
||||
|
||||
var builds = JsonConvert.DeserializeObject<List<BuildFile>>(compiledBuilds);
|
||||
return builds;
|
||||
}
|
||||
|
||||
public async Task<Result<DateTime, Exception>> GetLastUpdateTime()
|
||||
{
|
||||
var maybeDriveItem = await this.GetDriveItem();
|
||||
if (maybeDriveItem.ExtractValue() is not DriveItem driveItem)
|
||||
{
|
||||
return new InvalidOperationException("Unable to retrieve the drive file");
|
||||
}
|
||||
|
||||
return driveItem.LastModifiedDateTime;
|
||||
}
|
||||
|
||||
public void ResetAuthorization()
|
||||
{
|
||||
this.ResetAccessToken();
|
||||
}
|
||||
|
||||
private async Task<Optional<string>> GetBackupItemContent()
|
||||
{
|
||||
var maybeDriveItem = await this.GetDriveItem();
|
||||
if (maybeDriveItem.ExtractValue() is not DriveItem driveItem)
|
||||
{
|
||||
return Optional.None<string>();
|
||||
}
|
||||
|
||||
if (driveItem.Name != BackupFileName)
|
||||
{
|
||||
return Optional.None<string>();
|
||||
}
|
||||
|
||||
var response = await this.httpClient.GetAsync(driveItem.DownloadUrl);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
return Optional.None<string>();
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> UploadBackupItem(string serializedBuilds)
|
||||
{
|
||||
using var stringContent = new StringContent(serializedBuilds);
|
||||
var response = await this.httpClient.PutAsync(SyncFileUri + ContentSuffix, stringContent);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
private async Task<Optional<DriveItem>> GetDriveItem()
|
||||
{
|
||||
var response = await this.httpClient.GetAsync(SyncFileUri);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
return Optional.None<DriveItem>();
|
||||
}
|
||||
|
||||
var driveItemContent = await response.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<DriveItem>(driveItemContent);
|
||||
}
|
||||
|
||||
private async Task<Result<string, Exception>> RetrieveAuthorizationCode(
|
||||
ChromiumBrowserWrapper chromiumBrowserWrapper,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
chromiumBrowserWrapper.ThrowIfNull();
|
||||
await chromiumBrowserWrapper.InitializeDefaultBrowser();
|
||||
|
||||
var state = GetNewState();
|
||||
|
||||
chromiumBrowserWrapper.Address = AuthorizationUrlPlaceholder
|
||||
.Replace(ClientIdPlaceholder, ApplicationId)
|
||||
.Replace(RedirectUriPlaceholder, RedirectUri
|
||||
.Replace(":", "%3a")
|
||||
.Replace("/", "%2f"))
|
||||
.Replace(ScopesPlaceholder, Scopes
|
||||
.Replace(' ', '+'))
|
||||
.Replace(StatePlaceholder, state);
|
||||
|
||||
while (chromiumBrowserWrapper.Address.StartsWith(RedirectUri) is false)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return new TaskCanceledException();
|
||||
}
|
||||
|
||||
await Task.Delay(1000).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
var query = HttpUtility.ParseQueryString(chromiumBrowserWrapper.Address.Split('?').Skip(1).FirstOrDefault());
|
||||
if (query.GetValues(QueryStateKey) is string[] states is false)
|
||||
{
|
||||
throw new InvalidOperationException("Response doesn't have state key in response");
|
||||
}
|
||||
|
||||
if (states.Length < 1 || states.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException("Response contains invalid state");
|
||||
}
|
||||
|
||||
if (states.First() != state)
|
||||
{
|
||||
throw new InvalidOperationException("Response contains incorrect state");
|
||||
}
|
||||
|
||||
if (query.GetValues(QueryCodeKey) is string[] codes is false)
|
||||
{
|
||||
throw new InvalidOperationException("Response doesn't have code key in response");
|
||||
}
|
||||
|
||||
if (codes.Length < 1 || codes.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException("Response contains invalid code");
|
||||
}
|
||||
|
||||
var authCode = codes.First();
|
||||
return authCode;
|
||||
}
|
||||
|
||||
private async Task<Result<TokenResponse, Exception>> RetrieveAccessToken(string authorizationCode)
|
||||
{
|
||||
using var formContent = new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code", authorizationCode },
|
||||
{ "redirect_uri", RedirectUri },
|
||||
{ "client_id", ApplicationId },
|
||||
{ "scope", Scopes }
|
||||
});
|
||||
|
||||
using var httpRequest = new HttpRequestMessage
|
||||
{
|
||||
Content = formContent,
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(TokenUrlPlaceholder)
|
||||
};
|
||||
|
||||
using var response = await this.httpClient.SendAsync(httpRequest);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
return new InvalidOperationException($"Invalid access token response. Status code [{response.StatusCode}]");
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<TokenResponse>(await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private void ResetAccessToken()
|
||||
{
|
||||
this.liveUpdateableOptions.Value.ProtectedGraphAccessToken = null;
|
||||
this.liveUpdateableOptions.UpdateOption();
|
||||
}
|
||||
|
||||
private void SaveAccessToken(AccessToken token)
|
||||
{
|
||||
var codeBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(token));
|
||||
this.liveUpdateableOptions.Value.ProtectedGraphAccessToken = Convert.ToBase64String(ProtectedData.Protect(codeBytes, Entropy, DataProtectionScope.CurrentUser));
|
||||
this.liveUpdateableOptions.UpdateOption();
|
||||
}
|
||||
|
||||
private Optional<AccessToken> LoadAccessToken()
|
||||
{
|
||||
var protectedCode = this.liveUpdateableOptions.Value.ProtectedGraphAccessToken;
|
||||
if (protectedCode.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Optional.None<AccessToken>();
|
||||
}
|
||||
|
||||
var codeBytes = ProtectedData.Unprotect(Convert.FromBase64String(protectedCode), Entropy, DataProtectionScope.CurrentUser);
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<AccessToken>(Encoding.UTF8.GetString(codeBytes));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
this.logger.LogError(e, "Failed to load access token. Resetting access token");
|
||||
this.ResetAccessToken();
|
||||
return Optional.None<AccessToken>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetNewState()
|
||||
{
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Services.Graph.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Extensions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Daybreak.Services.Graph;
|
||||
|
||||
public interface IGraphClient
|
||||
{
|
||||
Task<Result<User, Exception>> GetUserProfile<TViewType>()
|
||||
where TViewType : UserControl;
|
||||
Task<Result<bool, Exception>> PerformAuthorizationFlow(ChromiumBrowserWrapper chromiumBrowserWrapper, CancellationToken cancellationToken = default);
|
||||
Task<Result<bool, Exception>> UploadBuilds();
|
||||
Task<Result<bool, Exception>> DownloadBuilds();
|
||||
Task<Result<List<BuildFile>, Exception>> RetrieveBuildsList();
|
||||
Task<Result<DateTime, Exception>> GetLastUpdateTime();
|
||||
void ResetAuthorization();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class AccessToken
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
|
||||
public static AccessToken FromTokenResponse(TokenResponse tokenResponse)
|
||||
{
|
||||
return new AccessToken
|
||||
{
|
||||
Token = tokenResponse.AccessToken,
|
||||
ExpirationDate = DateTime.Now + TimeSpan.FromSeconds(tokenResponse.ExpiresIn)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class BuildFile
|
||||
{
|
||||
public string TemplateCode { get; set; }
|
||||
public string FileName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class BuildFilesPayload
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class DriveItem
|
||||
{
|
||||
[JsonProperty("@microsoft.graph.downloadUrl")]
|
||||
public string DownloadUrl { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("lastModifiedDateTime")]
|
||||
public DateTime LastModifiedDateTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class TokenResponse
|
||||
{
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
[JsonProperty("scope")]
|
||||
public string Scope { get; set; }
|
||||
[JsonProperty("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class User
|
||||
{
|
||||
[JsonProperty("displayName")]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[JsonProperty("mail")]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Daybreak.Services.Graph.Models;
|
||||
|
||||
public sealed class ViewRedirectContext
|
||||
{
|
||||
public Type CallingView { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Models;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
using System.Threading;
|
||||
|
||||
namespace Daybreak.Services.IconRetrieve
|
||||
{
|
||||
public interface IIconBrowser
|
||||
{
|
||||
void InitializeWebView(WebView2 webView2, CancellationToken cancellationToken);
|
||||
void InitializeWebView(ChromiumBrowserWrapper webView2, CancellationToken cancellationToken);
|
||||
/// <summary>
|
||||
/// Queue an icon request. The browser will attempt to download the icon. Monitor the <see cref="IconRequest.Finished"/> to be notified when the request has been served.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Models.Progress;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Daybreak.Services.IconRetrieve
|
||||
{
|
||||
public interface IIconDownloader
|
||||
{
|
||||
void SetBrowser(WebView2 chromiumBrowserWrapper);
|
||||
void SetBrowser(ChromiumBrowserWrapper chromiumBrowserWrapper);
|
||||
|
||||
bool DownloadComplete { get; }
|
||||
Task<IconDownloadStatus> StartIconDownload();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Configuration;
|
||||
using Daybreak.Controls;
|
||||
using Daybreak.Models;
|
||||
using Daybreak.Models.Builds;
|
||||
using Daybreak.Utils;
|
||||
@@ -8,6 +9,7 @@ using Microsoft.Web.WebView2.Wpf;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Configuration;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.IO;
|
||||
@@ -31,7 +33,8 @@ namespace Daybreak.Services.IconRetrieve
|
||||
|
||||
private readonly ConcurrentQueue<IconRequest> iconRequests = new();
|
||||
private readonly ILogger<IconBrowser> logger;
|
||||
private WebView2 browserWrapper;
|
||||
|
||||
private ChromiumBrowserWrapper browserWrapper;
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
public IconBrowser(
|
||||
@@ -40,7 +43,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
this.logger = logger.ThrowIfNull();
|
||||
}
|
||||
|
||||
public void InitializeWebView(WebView2 webView2, CancellationToken cancellationToken)
|
||||
public void InitializeWebView(ChromiumBrowserWrapper webView2, CancellationToken cancellationToken)
|
||||
{
|
||||
this.browserWrapper = webView2.ThrowIfNull();
|
||||
this.cancellationToken = cancellationToken;
|
||||
@@ -91,7 +94,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
|
||||
try
|
||||
{
|
||||
await this.browserWrapper.EnsureCoreWebView2Async();
|
||||
await this.browserWrapper.InitializeDefaultBrowser();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -104,7 +107,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
var skillIconUrl = $"{BaseUrl}/{QueryUrl.Replace(NamePlaceholder, curedSkillName)}";
|
||||
logger.LogInformation($"Looking for icon at {skillIconUrl}");
|
||||
|
||||
this.browserWrapper.CoreWebView2.Navigate(skillIconUrl);
|
||||
this.browserWrapper.Address = skillIconUrl;
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
@@ -116,7 +119,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
logger.LogInformation("Executing extraction script");
|
||||
var responseTask = await Application.Current.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
return await this.browserWrapper.ExecuteScriptAsync(Scripts.GetHrefFromSkillPage);
|
||||
return await this.browserWrapper.WebBrowser.ExecuteScriptAsync(Scripts.GetHrefFromSkillPage);
|
||||
});
|
||||
var response = await responseTask;
|
||||
logger.LogInformation("Parsing response");
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
private readonly ILogger<IconDownloader> logger;
|
||||
private readonly ILiveOptions<ApplicationConfiguration> liveOptions;
|
||||
private readonly ILogger<ChromiumBrowserWrapper> browserLogger;
|
||||
private WebView2 browserWrapper;
|
||||
private ChromiumBrowserWrapper browserWrapper;
|
||||
private CancellationTokenSource cancellationTokenSource;
|
||||
private IconDownloadStatus iconDownloadStatus;
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
this.HookIntoConfigurationChanges();
|
||||
}
|
||||
|
||||
public void SetBrowser(WebView2 chromiumBrowserWrapper)
|
||||
public void SetBrowser(ChromiumBrowserWrapper chromiumBrowserWrapper)
|
||||
{
|
||||
if (this.browserWrapper is not null)
|
||||
{
|
||||
@@ -179,7 +179,7 @@ namespace Daybreak.Services.IconRetrieve
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
this.browserWrapper.IsEnabled = false;
|
||||
this.browserWrapper.Dispose();
|
||||
this.browserWrapper.WebBrowser.Dispose();
|
||||
});
|
||||
this.iconDownloadStatus.CurrentStep = IconDownloadStatus.Finished;
|
||||
this.DownloadComplete = true;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
Clicked="BackButton_Clicked"></controls:BackButton>
|
||||
<controls:AddButton Foreground="White" Height="30" Width="30" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5"
|
||||
Clicked="AddButton_Clicked"></controls:AddButton>
|
||||
<controls:SynchronizeButton Foreground="White" Height="30" Width="30" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5, 5, 45, 5"
|
||||
Clicked="SynchronizeButton_Clicked"></controls:SynchronizeButton>
|
||||
<controls:SearchTextBox Grid.Row="1"
|
||||
FontSize="24"
|
||||
Foreground="White"
|
||||
|
||||
@@ -67,5 +67,10 @@ namespace Daybreak.Views
|
||||
this.BuildEntries.AddRange(
|
||||
this.buildEntries.Where(b => StringUtils.MatchesSearchString(b.Name, e)));
|
||||
}
|
||||
|
||||
private void SynchronizeButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
this.viewManager.ShowView<BuildsSynchronizationView>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<UserControl x:Class="Daybreak.Views.BuildsSynchronizationView"
|
||||
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:local="clr-namespace:Daybreak.Views"
|
||||
xmlns:controls="clr-namespace:Daybreak.Controls"
|
||||
Loaded="UserControl_Loaded"
|
||||
mc:Ignorable="d"
|
||||
x:Name="_this"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid
|
||||
Background="#A0202020">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<controls:BackButton Foreground="White" Height="30" Width="30" Grid.Column="0" HorizontalAlignment="Left" Margin="5"
|
||||
Clicked="BackButton_Clicked" VerticalAlignment="Top"
|
||||
IsEnabled="{Binding ElementName=_this, Path=ButtonsEnabled, Mode=OneWay}"></controls:BackButton>
|
||||
<controls:BackButton Foreground="White" Height="30" Width="30" HorizontalAlignment="Right" Margin="0, 5, 45, 5"
|
||||
Clicked="DownloadButton_Clicked"
|
||||
IsEnabled="{Binding ElementName=_this, Path=ButtonsEnabled, Mode=OneWay}">
|
||||
<controls:BackButton.RenderTransform>
|
||||
<RotateTransform Angle="270" CenterX="15" CenterY="15"></RotateTransform>
|
||||
</controls:BackButton.RenderTransform>
|
||||
</controls:BackButton>
|
||||
<controls:BackButton Foreground="White" Height="30" Width="30" HorizontalAlignment="Right" Margin="0, 5, 5, 5"
|
||||
Clicked="UploadButton_Clicked"
|
||||
IsEnabled="{Binding ElementName=_this, Path=ButtonsEnabled, Mode=OneWay}">
|
||||
<controls:BackButton.RenderTransform>
|
||||
<RotateTransform Angle="90" CenterX="15" CenterY="15"></RotateTransform>
|
||||
</controls:BackButton.RenderTransform>
|
||||
</controls:BackButton>
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="Build templates synchronization"
|
||||
FontSize="24"
|
||||
Foreground="White" />
|
||||
<WrapPanel HorizontalAlignment="Center"
|
||||
Grid.Row="1">
|
||||
<TextBlock Text="Logged in as: "
|
||||
FontSize="20"
|
||||
Foreground="White"></TextBlock>
|
||||
<TextBlock Text="{Binding ElementName=_this, Path=DisplayName, Mode=OneWay}"
|
||||
FontSize="20"
|
||||
Foreground="White"></TextBlock>
|
||||
</WrapPanel>
|
||||
<WrapPanel HorizontalAlignment="Center"
|
||||
Grid.Row="2">
|
||||
<TextBlock Text="Last uploaded: "
|
||||
Foreground="White"
|
||||
FontSize="18"/>
|
||||
<TextBlock Text="{Binding ElementName=_this, Path=LastUploadDate, Mode=OneWay}"
|
||||
Foreground="White"
|
||||
FontSize="20"/>
|
||||
</WrapPanel>
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="Preview uploaded templates"
|
||||
FontSize="14"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"/>
|
||||
<ListView Grid.Row="1" Background="Transparent" ItemsSource="{Binding ElementName=_this, Path=BuildEntries, Mode=OneWay}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<TextBlock FontSize="14"
|
||||
Foreground="White"
|
||||
Text="{Binding FileName}"
|
||||
HorizontalAlignment="Left"></TextBlock>
|
||||
<TextBlock FontSize="14"
|
||||
Foreground="White"
|
||||
Text="{Binding TemplateCode}"
|
||||
HorizontalAlignment="Right"></TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,117 @@
|
||||
using Daybreak.Services.Graph;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Core.Extensions;
|
||||
using System.Windows;
|
||||
using System.Windows.Extensions;
|
||||
using System.Windows.Controls;
|
||||
using Daybreak.Services.ViewManagement;
|
||||
using System.Collections.ObjectModel;
|
||||
using Daybreak.Services.Graph.Models;
|
||||
using System.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace Daybreak.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for BuildsSynchronizationView.xaml
|
||||
/// </summary>
|
||||
public partial class BuildsSynchronizationView : UserControl
|
||||
{
|
||||
private readonly IGraphClient graphClient;
|
||||
private readonly IViewManager viewManager;
|
||||
private readonly ILogger<BuildsSynchronizationView> logger;
|
||||
|
||||
public ObservableCollection<BuildFile> BuildEntries { get; } = new();
|
||||
|
||||
[GenerateDependencyProperty(InitialValue = true)]
|
||||
private bool buttonsEnabled;
|
||||
[GenerateDependencyProperty]
|
||||
private string displayName;
|
||||
[GenerateDependencyProperty]
|
||||
private string lastUploadDate;
|
||||
|
||||
public BuildsSynchronizationView(
|
||||
IGraphClient graphClient,
|
||||
IViewManager viewManager,
|
||||
ILogger<BuildsSynchronizationView> logger)
|
||||
{
|
||||
this.graphClient = graphClient.ThrowIfNull();
|
||||
this.viewManager = viewManager.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.ButtonsEnabled = false;
|
||||
var profile = await this.graphClient.GetUserProfile<BuildsSynchronizationView>();
|
||||
if (profile.TryExtractSuccess(out var user) is false)
|
||||
{
|
||||
this.logger.LogError("Failed to get user info");
|
||||
return;
|
||||
}
|
||||
|
||||
this.DisplayName = user.DisplayName;
|
||||
await this.PopulateLastUpdateTime();
|
||||
await this.PopulateBuilds();
|
||||
this.ButtonsEnabled = true;
|
||||
}
|
||||
|
||||
private async Task PopulateLastUpdateTime()
|
||||
{
|
||||
var maybeDateTime = await this.graphClient.GetLastUpdateTime();
|
||||
if (maybeDateTime.TryExtractSuccess(out var dateTime))
|
||||
{
|
||||
this.LastUploadDate = dateTime.ToString("G");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LastUploadDate = "Never";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PopulateBuilds()
|
||||
{
|
||||
var maybeBuilds = await this.graphClient.RetrieveBuildsList();
|
||||
if (maybeBuilds.TryExtractSuccess(out var builds))
|
||||
{
|
||||
this.BuildEntries.ClearAnd().AddRange(builds);
|
||||
}
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
this.viewManager.ShowView<BuildsListView>();
|
||||
}
|
||||
|
||||
private async void UploadButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
this.ButtonsEnabled = false;
|
||||
var result = await this.graphClient.UploadBuilds();
|
||||
result.DoAny(
|
||||
onFailure: (failure) =>
|
||||
{
|
||||
this.logger.LogError(failure, $"Failed to upload builds");
|
||||
});
|
||||
|
||||
await this.PopulateLastUpdateTime();
|
||||
await this.PopulateBuilds();
|
||||
this.ButtonsEnabled = true;
|
||||
}
|
||||
|
||||
private async void DownloadButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
this.ButtonsEnabled = false;
|
||||
var result = await this.graphClient.DownloadBuilds();
|
||||
result.DoAny(
|
||||
onFailure: (failure) =>
|
||||
{
|
||||
this.logger.LogError(failure, $"Failed to download builds");
|
||||
});
|
||||
|
||||
await this.PopulateLastUpdateTime();
|
||||
await this.PopulateBuilds();
|
||||
this.ButtonsEnabled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<UserControl x:Class="Daybreak.Views.GraphAuthorizationView"
|
||||
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:local="clr-namespace:Daybreak.Views"
|
||||
xmlns:controls="clr-namespace:Daybreak.Controls"
|
||||
Loaded="UserControl_Loaded"
|
||||
Unloaded="UserControl_Unloaded"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid
|
||||
Background="#A0202020">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Text="Please login to access OneDrive files"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="24"
|
||||
Foreground="White"/>
|
||||
<controls:ChromiumBrowserWrapper
|
||||
x:Name="BrowserWrapper"
|
||||
Grid.Row="1"
|
||||
ControlsEnabled="False"
|
||||
CanNavigate="True"
|
||||
CanDownloadBuild="False"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,59 @@
|
||||
using Daybreak.Services.Graph;
|
||||
using Daybreak.Services.Graph.Models;
|
||||
using Daybreak.Services.ViewManagement;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Core.Extensions;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Daybreak.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for GraphAuthorizationView.xaml
|
||||
/// </summary>
|
||||
public partial class GraphAuthorizationView : UserControl
|
||||
{
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
private readonly IGraphClient graphClient;
|
||||
private readonly IViewManager viewManager;
|
||||
private readonly ILogger<GraphAuthorizationView> logger;
|
||||
|
||||
public GraphAuthorizationView(
|
||||
IGraphClient graphClient,
|
||||
IViewManager viewManager,
|
||||
ILogger<GraphAuthorizationView> logger)
|
||||
{
|
||||
this.graphClient = graphClient.ThrowIfNull();
|
||||
this.viewManager = viewManager.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private async void UserControl_Loaded(object _, RoutedEventArgs e)
|
||||
{
|
||||
var authorizationResult = await this.graphClient.PerformAuthorizationFlow(this.BrowserWrapper, this.cancellationTokenSource.Token);
|
||||
if (authorizationResult.TryExtractFailure(out var failure))
|
||||
{
|
||||
this.logger.LogError(failure, "Authorization failed");
|
||||
this.viewManager.ShowView<MainView>();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.DataContext is not ViewRedirectContext redirectContext)
|
||||
{
|
||||
this.logger.LogError("Cannot redirect to proper view. No view set in context");
|
||||
this.viewManager.ShowView<MainView>();
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewManager.ShowView(redirectContext.CallingView);
|
||||
}
|
||||
|
||||
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,6 @@ namespace Daybreak.Views
|
||||
private readonly IViewManager viewManager;
|
||||
private readonly ILiveUpdateableOptions<ApplicationConfiguration> liveOptions;
|
||||
private readonly IScreenManager screenManager;
|
||||
private readonly IBuildTemplateManager buildTemplateManager;
|
||||
private readonly ILogger<ChromiumBrowserWrapper> browserLogger;
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
|
||||
private bool leftBrowserMaximized = false;
|
||||
@@ -60,12 +58,8 @@ namespace Daybreak.Views
|
||||
IApplicationLauncher applicationDetector,
|
||||
IViewManager viewManager,
|
||||
ILiveUpdateableOptions<ApplicationConfiguration> liveOptions,
|
||||
IScreenManager screenManager,
|
||||
IBuildTemplateManager buildTemplateManager,
|
||||
ILogger<ChromiumBrowserWrapper> browserLogger)
|
||||
IScreenManager screenManager)
|
||||
{
|
||||
this.browserLogger = browserLogger;
|
||||
this.buildTemplateManager = buildTemplateManager.ThrowIfNull(nameof(buildTemplateManager));
|
||||
this.screenManager = screenManager.ThrowIfNull(nameof(screenManager));
|
||||
this.liveOptions = liveOptions.ThrowIfNull(nameof(liveOptions));
|
||||
this.applicationDetector = applicationDetector.ThrowIfNull(nameof(applicationDetector));
|
||||
@@ -77,8 +71,8 @@ namespace Daybreak.Views
|
||||
|
||||
private async void InitializeBrowsers()
|
||||
{
|
||||
await this.LeftWebBrowser.InitializeBrowser(this.liveOptions, this.buildTemplateManager, this.browserLogger);
|
||||
await this.RightWebBrowser.InitializeBrowser(this.liveOptions, this.buildTemplateManager, this.browserLogger);
|
||||
await this.LeftWebBrowser.InitializeDefaultBrowser();
|
||||
await this.RightWebBrowser.InitializeDefaultBrowser();
|
||||
this.NavigateToDefaults();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user