Skip to content
This repository has been archived by the owner on Mar 23, 2024. It is now read-only.

Commit

Permalink
Merge pull request #25 from kc3hack/feat/#22
Browse files Browse the repository at this point in the history
#22 APIキーの設定を追加
  • Loading branch information
aiueo-1234 authored Feb 22, 2024
2 parents 9f1ded9 + 65b28f4 commit f6b55e6
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 62 deletions.
8 changes: 8 additions & 0 deletions KoeBook.Core/Contracts/Services/ISecretSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace KoeBook.Core.Contracts.Services;

public interface ISecretSettingsService
{
Task<string?> GetApiKeyAsync(string folderPath, CancellationToken cancellationToken);

Task SaveApiKeyAsync(string folderPath, string apiKey, CancellationToken cancellationToken);
}
1 change: 1 addition & 0 deletions KoeBook.Core/KoeBook.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="FastEnum" Version="1.8.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
49 changes: 49 additions & 0 deletions KoeBook.Core/Services/SercretSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma warning disable CA1416 // プラットフォームの互換性を検証
using System.Security.Cryptography;
using System.Text;
using KoeBook.Core.Contracts.Services;

namespace KoeBook.Core.Services;

public class SecretSettingsService : ISecretSettingsService
{
private readonly byte[] _bytes;

public SecretSettingsService()
{
ulong value = 2546729043367843253ul;
value ^= value << 13;
value ^= value >> 7;
value ^= value << 17;
_bytes = BitConverter.GetBytes(value);
}

public Task<string?> GetApiKeyAsync(string folderPath, CancellationToken cancellationToken)
{
return Task.Run(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var data = await File.ReadAllBytesAsync(Path.Combine(folderPath, "alt"), cancellationToken).ConfigureAwait(false);
if (data is null)
return null;

var result = ProtectedData.Unprotect(data, _bytes, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(result);
}, cancellationToken);
}

public Task SaveApiKeyAsync(string folderPath, string apiKey, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(apiKey);
return Task.Run(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var data = Encoding.UTF8.GetBytes(apiKey);

var result = ProtectedData.Protect(data, _bytes, DataProtectionScope.CurrentUser);
if (!Directory.Exists(folderPath))
Directory.CreateDirectory(folderPath);
await File.WriteAllBytesAsync(Path.Combine(folderPath, "alt"), result, cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
}
1 change: 1 addition & 0 deletions KoeBook/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public App()

// Core Services
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<ISecretSettingsService, SecretSettingsService>();

// Views and ViewModels
services.AddTransient<SettingsViewModel>();
Expand Down
4 changes: 4 additions & 0 deletions KoeBook/Contracts/Services/ILocalSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public interface ILocalSettingsService
Task<T?> ReadSettingAsync<T>(string key);

Task SaveSettingAsync<T>(string key, T value);

ValueTask<string?> GetApiKeyAsync(CancellationToken cancellationToken);

ValueTask SaveApiKeyAsync(string apiKey, CancellationToken cancellationToken);
}
16 changes: 13 additions & 3 deletions KoeBook/Services/LocalSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
using KoeBook.Models;

using Microsoft.Extensions.Options;

using Windows.ApplicationModel;
using Windows.Storage;

namespace KoeBook.Services;
Expand All @@ -18,6 +16,7 @@ public class LocalSettingsService : ILocalSettingsService

private readonly IFileService _fileService;
private readonly LocalSettingsOptions _options;
private readonly ISecretSettingsService _secretSettingsService;

private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
private readonly string _applicationDataFolder;
Expand All @@ -27,10 +26,11 @@ public class LocalSettingsService : ILocalSettingsService

private bool _isInitialized;

public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options, ISecretSettingsService secretSettingsService)
{
_fileService = fileService;
_options = options.Value;
_secretSettingsService = secretSettingsService;

_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? _defaultApplicationDataFolder);
_localsettingsFile = _options.LocalSettingsFile ?? _defaultLocalSettingsFile;
Expand Down Expand Up @@ -85,4 +85,14 @@ public async Task SaveSettingAsync<T>(string key, T value)
await Task.Run(() => _fileService.Save(_applicationDataFolder, _localsettingsFile, _settings));
}
}

public async ValueTask<string?> GetApiKeyAsync(CancellationToken cancellationToken)
{
return await _secretSettingsService.GetApiKeyAsync(_applicationDataFolder, cancellationToken).ConfigureAwait(false);
}

public async ValueTask SaveApiKeyAsync(string apiKey, CancellationToken cancellationToken)
{
await _secretSettingsService.SaveApiKeyAsync(_applicationDataFolder, apiKey, cancellationToken).ConfigureAwait(false);
}
}
61 changes: 47 additions & 14 deletions KoeBook/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
using CommunityToolkit.Mvvm.Input;

using KoeBook.Contracts.Services;
using KoeBook.Core.Contracts.Services;
using KoeBook.Helpers;

using Microsoft.UI.Xaml;

using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel;

namespace KoeBook.ViewModels;
Expand All @@ -17,29 +18,61 @@ public partial class SettingsViewModel : ObservableRecipient
{
private readonly IThemeSelectorService _themeSelectorService;

private readonly ILocalSettingsService _localSettingsService;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedThemeIndex))]
private ElementTheme _elementTheme;

public int SelectedThemeIndex
{
get => (int)ElementTheme;
set => ElementTheme = (ElementTheme)value;
}

[ObservableProperty]
private string _versionDescription;
private string _apiKey = string.Empty;

public ICommand SwitchThemeCommand { get; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ApiKeyRevealMode))]
[NotifyPropertyChangedFor(nameof(ApiKeyDescription))]
private bool _revealApiKey;

public PasswordRevealMode ApiKeyRevealMode => RevealApiKey ? PasswordRevealMode.Visible : PasswordRevealMode.Hidden;

public string ApiKeyDescription => RevealApiKey ? "表示" : "非表示";

[ObservableProperty]
private string _versionDescription;

public SettingsViewModel(IThemeSelectorService themeSelectorService)
public SettingsViewModel(IThemeSelectorService themeSelectorService, ILocalSettingsService localSettingsService)
{
_themeSelectorService = themeSelectorService;
_elementTheme = _themeSelectorService.Theme;
_localSettingsService = localSettingsService;
_versionDescription = GetVersionDescription();
}

public async void OnThemeChangedAsync(object _, SelectionChangedEventArgs __)
{
await _themeSelectorService.SetThemeAsync(ElementTheme);
}

partial void OnApiKeyChanged(string value)
{
Core(_localSettingsService, value);

SwitchThemeCommand = new RelayCommand<ElementTheme>(
async (param) =>
{
if (ElementTheme != param)
{
ElementTheme = param;
await _themeSelectorService.SetThemeAsync(param);
}
});
static async void Core(ILocalSettingsService service, string value)
{
await service.SaveApiKeyAsync(value, default);
}
}

public async void OnLoaded(object _, RoutedEventArgs __)
{
var key = await _localSettingsService.GetApiKeyAsync(default);
if (key is not null)
ApiKey = key;
}

private static string GetVersionDescription()
Expand All @@ -57,6 +90,6 @@ private static string GetVersionDescription()
version = Assembly.GetExecutingAssembly().GetName().Version!;
}

return $"{"AppDisplayName".GetLocalized()} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
return $"KoeBook - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
94 changes: 49 additions & 45 deletions KoeBook/Views/SettingsPage.xaml
Original file line number Diff line number Diff line change
@@ -1,66 +1,70 @@
<Page
<Page
x:Class="KoeBook.Views.SettingsPage"
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"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:helpers="using:KoeBook.Helpers"
xmlns:xaml="using:Microsoft.UI.Xaml"
mc:Ignorable="d">
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"
Loaded="{x:Bind ViewModel.OnLoaded}">
<Page.Resources>
<helpers:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
</Page.Resources>
<Grid>
<StackPanel
x:Name="ContentArea">
<TextBlock x:Uid="Settings_Personalization" Style="{ThemeResource SubtitleTextBlockStyle}" />
<StackPanel Margin="{StaticResource SmallTopBottomMargin}">
<TextBlock x:Uid="Settings_Theme" />
<TextBlock
Style="{ThemeResource TitleTextBlockStyle}"
Text="設定"/>

<StackPanel Margin="{StaticResource XSmallTopMargin}">
<RadioButton
x:Uid="Settings_Theme_Light"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Light, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Light</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
<RadioButton
x:Uid="Settings_Theme_Dark"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Dark, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Dark</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
<RadioButton
x:Uid="Settings_Theme_Default"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Default, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Default</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
</StackPanel>
<TextBlock
Margin="{StaticResource MediumTopMargin}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="外観と動作"/>

<StackPanel
Margin="{StaticResource XSmallTopMargin}"
Spacing="4">

<controls:SettingsCard
Header="テーマ"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<ComboBox
SelectedIndex="{x:Bind ViewModel.SelectedThemeIndex, Mode=TwoWay}"
SelectionChanged="ViewModel.OnThemeChangedAsync">
<ComboBoxItem>Windows の既定値</ComboBoxItem>
<ComboBoxItem>ライト</ComboBoxItem>
<ComboBoxItem>ダーク</ComboBoxItem>
</ComboBox>
</controls:SettingsCard>

<controls:SettingsCard
Header="GPT APIキー"
HeaderIcon="{ui:SymbolIcon Symbol=Permissions}">
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{x:Bind ViewModel.RevealApiKey, Mode=TwoWay}">
<TextBlock
TextAlignment="Center"
Text="{x:Bind ViewModel.ApiKeyDescription, Mode=OneWay}" />
</CheckBox>
<PasswordBox
MinWidth="600"
PasswordRevealMode="{x:Bind ViewModel.ApiKeyRevealMode, Mode=OneWay}"
Password="{x:Bind ViewModel.ApiKey, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"/>
</StackPanel>
</controls:SettingsCard>
</StackPanel>

<TextBlock x:Uid="Settings_About" Style="{ThemeResource SubtitleTextBlockStyle}" />
<TextBlock
Text="アプリ情報"
Margin="{StaticResource SmallTopMargin}"
Style="{ThemeResource SubtitleTextBlockStyle}" />

<StackPanel Margin="{StaticResource XSmallTopMargin}">
<TextBlock Text="{x:Bind ViewModel.VersionDescription, Mode=OneWay}" Style="{ThemeResource BodyTextBlockStyle}" />

<TextBlock
x:Uid="Settings_AboutDescription"
Margin="{StaticResource XSmallTopMargin}"
Style="{ThemeResource BodyTextBlockStyle}" />
<HyperlinkButton x:Uid="SettingsPage_PrivacyTermsLink" Margin="{StaticResource SettingsPageHyperlinkButtonMargin}" />
</StackPanel>
</StackPanel>
</Grid>
Expand Down

0 comments on commit f6b55e6

Please sign in to comment.