Skip to content

Commit

Permalink
Merge pull request #39 from Freeesia/feature/check_update
Browse files Browse the repository at this point in the history
自動更新の仕組み追加
  • Loading branch information
Freeesia authored Apr 6, 2024
2 parents 5f293f4 + 5a5f040 commit 8893fce
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 38 deletions.
10 changes: 10 additions & 0 deletions VdLabel/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ namespace VdLabel;
/// </summary>
public partial class App : Application
{
private readonly TaskCompletionSource tcs = new();
public App()
{
InitializeComponent();
}

protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
this.tcs.SetResult();
}

public Task WaitForStartupAsync()
=> this.tcs.Task;
}
6 changes: 4 additions & 2 deletions VdLabel/CommandLabelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

namespace VdLabel;

partial class CommandLabelService(IConfigStore configStore, IVirualDesktopService virualDesktopService, ILogger<CommandLabelService> logger) : BackgroundService, ICommandLabelService
partial class CommandLabelService(App app, IConfigStore configStore, IVirualDesktopService virualDesktopService, ILogger<CommandLabelService> logger) : BackgroundService, ICommandLabelService
{
private readonly App app = app;
private readonly IConfigStore configStore = configStore;
private readonly IVirualDesktopService virualDesktopService = virualDesktopService;
private readonly ILogger<CommandLabelService> logger = logger;
Expand All @@ -33,6 +34,7 @@ public async ValueTask<string> ExecuteCommand(string command, bool utf8, Cancell
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
PeriodicTimer? timer = null;
await this.app.WaitForStartupAsync();
while (!stoppingToken.IsCancellationRequested)
{
var config = await this.configStore.Load();
Expand Down Expand Up @@ -67,7 +69,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}


interface ICommandLabelService : IHostedService
interface ICommandLabelService
{
ValueTask<string> ExecuteCommand(string command, bool utf8, CancellationToken token = default);
}
60 changes: 49 additions & 11 deletions VdLabel/IConfigStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ interface IConfigStore

ValueTask<Config> Load();
ValueTask Save(Config config);

ValueTask<UpdateInfo?> LoadUpdateInfo();
ValueTask SaveUpdateInfo(UpdateInfo updateInfo);
}

class ConfigStore(ILogger<ConfigStore> logger) : IConfigStore
{
private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VdLabel", "config.json");
private static readonly string baseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VdLabel");
private static readonly string configPath = Path.Combine(baseDir, "config.json");
private static readonly string updateInfoPath = Path.Combine(baseDir, "update.json");

private static readonly JsonSerializerOptions options = new()
{
Converters = { new ColorJsonConverter() },
Expand All @@ -27,33 +33,63 @@ class ConfigStore(ILogger<ConfigStore> logger) : IConfigStore

public event EventHandler? Saved;

public ValueTask<Config> Load()
public async ValueTask<Config> Load()
{
Config? config = null;
try
{
if (File.Exists(this.path))
if (File.Exists(configPath))
{
using var fs = File.OpenRead(this.path);
config = JsonSerializer.Deserialize<Config>(fs, options);
using var fs = File.OpenRead(configPath);
config = await JsonSerializer.DeserializeAsync<Config>(fs, options).ConfigureAwait(false);
}
}
catch (Exception e)
{
this.logger.LogError(e, "設定の読み込みに失敗しました");
}
return new(config ?? new Config() { DesktopConfigs = { new() { Id = Guid.Empty } } });
return config ?? new Config() { DesktopConfigs = { new() { Id = Guid.Empty } } };
}

public async ValueTask<UpdateInfo?> LoadUpdateInfo()
{
try
{
if (File.Exists(updateInfoPath))
{
using var fs = File.OpenRead(updateInfoPath);
return await JsonSerializer.DeserializeAsync<UpdateInfo>(fs, options);
}
}
catch (Exception e)
{
this.logger.LogError(e, "更新情報の読み込みに失敗しました");
}
return null;
}

public ValueTask Save(Config config)
public async ValueTask Save(Config config)
{
Directory.CreateDirectory(Path.GetDirectoryName(this.path)!);
using (var fs = File.Create(this.path))
Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
using (var fs = File.Create(configPath))
{
JsonSerializer.Serialize(fs, config, options);
await JsonSerializer.SerializeAsync(fs, config, options);
}
this.Saved?.Invoke(this, EventArgs.Empty);
return default;
}

public async ValueTask SaveUpdateInfo(UpdateInfo updateInfo)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(updateInfoPath)!);
using var fs = File.Create(updateInfoPath);
await JsonSerializer.SerializeAsync(fs, updateInfo, options);
}
catch (Exception)
{
this.logger.LogError("更新情報の保存に失敗しました");
}
}

private class ColorJsonConverter : JsonConverter<Color>
Expand All @@ -64,3 +100,5 @@ public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOpt
=> writer.WriteStringValue(ColorTranslator.ToHtml(value));
}
}

record UpdateInfo(string Version, string Url, string? Path, DateTime CheckedAt, bool Skip);
43 changes: 42 additions & 1 deletion VdLabel/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using Wpf.Ui;
using Wpf.Ui.Extensions;
Expand All @@ -14,6 +15,7 @@ partial class MainViewModel : ObservableObject
private readonly IContentDialogService dialogService;
private readonly IVirualDesktopService virualDesktopService;
private readonly ICommandLabelService commandLabelService;
private readonly IUpdateChecker updateChecker;

[ObservableProperty]
private bool isBusy;
Expand All @@ -27,24 +29,55 @@ partial class MainViewModel : ObservableObject
[ObservableProperty]
private bool isStartup;

[ObservableProperty]
private bool hasUpdate;

[ObservableProperty]
private string? newVersion;

private string? newVersionUrl;
private string? installPath;

public string Title { get; } = $"VdLabel {Assembly.GetExecutingAssembly().GetName().Version}";

public ObservableCollection<DesktopConfigViewModel> DesktopConfigs { get; } = [];

public IReadOnlyList<OverlayPosition> OverlayPositions { get; } = Enum.GetValues<OverlayPosition>();
public IReadOnlyList<NamePosition> NamePositions { get; } = Enum.GetValues<NamePosition>();

public MainViewModel(IConfigStore configStore, IContentDialogService dialogService, IVirualDesktopService virualDesktopService, ICommandLabelService commandLabelService)
public MainViewModel(
IConfigStore configStore,
IContentDialogService dialogService,
IVirualDesktopService virualDesktopService,
ICommandLabelService commandLabelService,
IUpdateChecker updateChecker)
{
this.configStore = configStore;
this.dialogService = dialogService;
this.virualDesktopService = virualDesktopService;
this.commandLabelService = commandLabelService;
this.updateChecker = updateChecker;
this.virualDesktopService.DesktopChanged += VirualDesktopService_DesktopChanged;
this.updateChecker.UpdateAvailable += UpdateChecker_UpdateAvailable;
this.isStartup = GetIsStartup();
SetUpUpdateInfo();
Load();
}

private void UpdateChecker_UpdateAvailable(object? sender, EventArgs e)
=> SetUpUpdateInfo();

private async void SetUpUpdateInfo()
{
if (this.updateChecker.HasUpdate && await this.configStore.LoadUpdateInfo() is { } info && !info.Skip)
{
this.NewVersion = info.Version;
this.newVersionUrl = info.Url;
this.installPath = info.Path;
this.HasUpdate = true;
}
}

private void VirualDesktopService_DesktopChanged(object? sender, DesktopChangedEventArgs e)
=> this.SelectedDesktopConfig = this.DesktopConfigs.FirstOrDefault(c => c.Id == e.DesktopId) ?? this.DesktopConfigs.FirstOrDefault();

Expand Down Expand Up @@ -107,6 +140,14 @@ public async Task ReloadDesktops()
}
}

[RelayCommand]
public void OpenReleaseNotes()
=> Process.Start(new ProcessStartInfo(this.newVersionUrl!) { UseShellExecute = true });

[RelayCommand]
public void InstallUpdate()
=> Process.Start("msiexec", $"/i {this.installPath}");

partial void OnIsStartupChanged(bool value)
{
var exe = Assembly.GetExecutingAssembly();
Expand Down
37 changes: 37 additions & 0 deletions VdLabel/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,43 @@
<ui:TitleBar.Icon>
<ui:ImageIcon Source="/app.ico" />
</ui:TitleBar.Icon>
<ui:TitleBar.Header>
<ui:DropDownButton
Width="44"
Height="30"
VerticalAlignment="Top"
VerticalContentAlignment="Stretch"
Background="Goldenrod"
BorderThickness="0"
CornerRadius="0"
Icon="{ui:SymbolIcon ArrowDownload48}"
MouseOverBackground="Gold"
Style="{StaticResource DefaultUiButtonStyle}"
Visibility="{Binding HasUpdate, Converter={StaticResource b2vConv}}">
<ui:DropDownButton.ToolTip>
<ui:TextBlock Text="{Binding NewVersion, StringFormat=新しいバージョン: {0} がリリースされました}" />
</ui:DropDownButton.ToolTip>
<ui:DropDownButton.Flyout>
<ContextMenu>
<ui:MenuItem Command="{Binding InstallUpdateCommand}">
<ui:MenuItem.Header>
<ui:TextBlock Text="{Binding NewVersion, StringFormat=新しいバージョン: {0} のインストール}" />
</ui:MenuItem.Header>
<ui:MenuItem.Icon>
<ui:SymbolIcon
Filled="True"
Foreground="LawnGreen"
Symbol="PresenceAvailable24" />
</ui:MenuItem.Icon>
</ui:MenuItem>
<ui:MenuItem
Command="{Binding OpenReleaseNotesCommand}"
Header="更新内容の確認"
Icon="{ui:SymbolIcon Globe24}" />
</ContextMenu>
</ui:DropDownButton.Flyout>
</ui:DropDownButton>
</ui:TitleBar.Header>
</ui:TitleBar>
<tray:NotifyIcon>
<tray:NotifyIcon.Menu>
Expand Down
3 changes: 2 additions & 1 deletion VdLabel/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
using Wpf.Ui.Appearance;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
using Wpf.Ui.Controls;

namespace VdLabel;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
public partial class MainWindow : FluentWindow
{
private readonly IContentDialogService contentDialogService;
private readonly IVirualDesktopService virualDesktopService;
Expand Down
1 change: 1 addition & 0 deletions VdLabel/OverlayViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public OverlayViewModel(Guid id, string name, IConfigStore configStore)
this.fontSize = config.FontSize;
this.overlaySize = config.OverlaySize;
this.foreground = config.Foreground;
this.background = config.Background;
this.duration = config.Duration;
this.position = config.NamePosition switch
{
Expand Down
13 changes: 9 additions & 4 deletions VdLabel/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@
var builder = KamishibaiApplication<App, MainWindow>.CreateBuilder();
builder.Services
.Configure<HostOptions>(op => op.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore)
.AddHostedService(sp => sp.GetRequiredService<IVirualDesktopService>())
.AddSingleton<VirtualDesktopService>()
.AddSingleton<CommandLabelService>()
.AddSingleton<UpdateChecker>()
.AddHostedService(sp => sp.GetRequiredService<VirtualDesktopService>())
.AddHostedService(sp => sp.GetRequiredService<CommandLabelService>())
.AddHostedService(sp => sp.GetRequiredService<UpdateChecker>())
.AddHostedService<WindowMonitor>()
.AddHostedService(sp => sp.GetRequiredService<ICommandLabelService>())
.AddSingleton<IVirualDesktopService, VirtualDesktopService>()
.AddSingleton<ICommandLabelService, CommandLabelService>()
.AddSingleton<IVirualDesktopService>(sp => sp.GetRequiredService<VirtualDesktopService>())
.AddSingleton<ICommandLabelService>(sp => sp.GetRequiredService<CommandLabelService>())
.AddSingleton<IUpdateChecker>(sp => sp.GetRequiredService<UpdateChecker>())
.AddSingleton<IConfigStore, ConfigStore>()
.AddSingleton<IContentDialogService, ContentDialogService>()
.AddPresentation<MainWindow, MainViewModel>()
Expand Down
Loading

0 comments on commit 8893fce

Please sign in to comment.