Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/windows-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Windows build

on:
push:
pull_request:
workflow_dispatch:

env:
BUILD_CONFIGURATION: Release

jobs:
credential-provider:
name: Credential Provider (${{ matrix.artifactName }})
runs-on: windows-2022
timeout-minutes: 60

strategy:
fail-fast: false
matrix:
include:
- platform: x64
artifactName: x64
- platform: Win32
artifactName: x86

steps:
- name: Check out sources
uses: actions/checkout@v4

- name: Locate MSBuild
uses: microsoft/setup-msbuild@v2

- name: Build solution
shell: pwsh
run: >
msbuild multiOTPCredentialProvider.sln
/m
/nologo
/p:Configuration=${{ env.BUILD_CONFIGURATION }}
/p:Platform=${{ matrix.platform }}
/p:PlatformToolset=v143
/bl:${{ github.workspace }}\msbuild-${{ matrix.artifactName }}.binlog

- name: Upload ${{ matrix.artifactName }} artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: multiotp-credential-provider-${{ matrix.artifactName }}
if-no-files-found: error
path: |
CredentialProvider/${{ matrix.platform }}/${{ env.BUILD_CONFIGURATION }}/*.dll
CredentialProviderFilter/${{ matrix.platform }}/${{ env.BUILD_CONFIGURATION }}/*.dll
CppClientCore/CppClientCore/${{ matrix.platform }}/${{ env.BUILD_CONFIGURATION }}/*.lib
WixInstall/multiOTPCredentialProviderInstaller/bin/${{ matrix.artifactName }}/${{ env.BUILD_CONFIGURATION }}/*.msi
msbuild-${{ matrix.artifactName }}.binlog

wpf-manager:
name: WPF manager
runs-on: windows-2022
timeout-minutes: 30

steps:
- name: Check out sources
uses: actions/checkout@v4

- name: Locate MSBuild
uses: microsoft/setup-msbuild@v2

- name: Install .NET Framework 4.5.2 Developer Pack
shell: pwsh
run: choco install netfx-4.5.2-devpack --no-progress -y

- name: Build manager
shell: pwsh
run: >
msbuild MultiOtpManager/MultiOtpManager.csproj
/m
/nologo
/p:Configuration=${{ env.BUILD_CONFIGURATION }}
/p:Platform=AnyCPU
/bl:${{ github.workspace }}\msbuild-manager.binlog

- name: Upload manager artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: multiotp-manager
if-no-files-found: error
path: |
MultiOtpManager/bin/${{ env.BUILD_CONFIGURATION }}/*
msbuild-manager.binlog
6 changes: 6 additions & 0 deletions MultiOtpManager/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>
5 changes: 5 additions & 0 deletions MultiOtpManager/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Application x:Class="MultiOtpManager.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>
66 changes: 66 additions & 0 deletions MultiOtpManager/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Globalization;
using System.IO;
using System.Threading;
using System.Windows;
using MultiOtpManager.Core;

namespace MultiOtpManager
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
// Pick the UI culture before any window is created so that
// MainWindow's {x:Static p:Resources.Key} bindings resolve against
// the chosen language.
AppSettings settings = AppSettings.Load();

// First-run auto-detection: if there is no settings file on disk,
// guess the best matching built-in language from the system UI
// culture and persist the choice so the picker reflects it later.
if (!File.Exists(AppSettings.SettingsFilePath))
{
settings.Language = GuessInitialLanguage();
settings.Save();
}

ApplyLanguage(settings.Language);

base.OnStartup(e);
}

private static string GuessInitialLanguage()
{
CultureInfo current = CultureInfo.CurrentUICulture;
string name = current != null ? current.Name : string.Empty;

// Anything in the zh family maps to Simplified Chinese; otherwise
// fall through to English, which is the Resources.resx fallback.
if (name.StartsWith("zh", System.StringComparison.OrdinalIgnoreCase))
{
return "zh-Hans";
}
return "en";
}

private static void ApplyLanguage(string cultureName)
{
if (string.IsNullOrEmpty(cultureName))
{
return;
}

try
{
CultureInfo culture = CultureInfo.GetCultureInfo(cultureName);
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
catch (CultureNotFoundException)
{
// Unknown culture name in settings: ignore and fall back to the
// process default, which will resolve to Resources.resx.
}
}
}
}
133 changes: 133 additions & 0 deletions MultiOtpManager/Core/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System;
using System.IO;

namespace MultiOtpManager.Core
{
/// <summary>
/// Per-user preferences persisted as JSON in %LocalAppData%. The file is
/// best-effort: corrupted or unreadable settings silently fall back to
/// defaults so a malformed disk file cannot stop the app from starting.
/// Serialization is hand-written to keep the project free of any
/// external JSON dependency (System.Text.Json is not available on
/// .NET Framework 4.5.2 without an extra reference assembly).
/// </summary>
public sealed class AppSettings
{
/// <summary>
/// UI culture name (for example "zh-Hans") or empty for system default.
/// Looked up via CultureInfo.GetCultureInfo during startup.
/// </summary>
public string Language { get; set; } = string.Empty;

private static readonly string SettingsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MultiOtpManager");

private static readonly string SettingsPath = Path.Combine(
SettingsDirectory, "settings.json");

public static string SettingsFilePath
{
get { return SettingsPath; }
}

public static AppSettings Load()
{
try
{
if (File.Exists(SettingsPath))
{
string text = File.ReadAllText(SettingsPath);
return Parse(text) ?? new AppSettings();
}
}
catch (Exception)
{
// Corrupted or unreadable; fall through to defaults.
}
return new AppSettings();
}

public void Save()
{
try
{
Directory.CreateDirectory(SettingsDirectory);
File.WriteAllText(SettingsPath, Serialize());
}
catch (Exception)
{
// Best-effort persistence; ignore write errors so a read-only
// profile does not break the rest of the app.
}
}

private string Serialize()
{
// Hand-rolled JSON keeps the file readable while avoiding any
// external JSON dependency. Language is the only field, so a
// constant template is sufficient.
return "{\n \"Language\": \"" + EscapeJsonString(Language ?? string.Empty) + "\"\n}\n";
}

private static AppSettings Parse(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return null;
}

AppSettings settings = new AppSettings();
int searchFrom = 0;
while (searchFrom < text.Length)
{
int keyStart = text.IndexOf('"', searchFrom);
if (keyStart < 0)
{
break;
}
int keyEnd = text.IndexOf('"', keyStart + 1);
if (keyEnd < 0)
{
break;
}
string key = text.Substring(keyStart + 1, keyEnd - keyStart - 1);

int colon = text.IndexOf(':', keyEnd);
if (colon < 0)
{
break;
}
int valueStart = text.IndexOf('"', colon);
if (valueStart < 0)
{
break;
}
int valueEnd = text.IndexOf('"', valueStart + 1);
if (valueEnd < 0)
{
break;
}
string value = UnescapeJsonString(
text.Substring(valueStart + 1, valueEnd - valueStart - 1));

if (key == "Language")
{
settings.Language = value;
}
searchFrom = valueEnd + 1;
}
return settings;
}

private static string EscapeJsonString(string value)
{
return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
}

private static string UnescapeJsonString(string value)
{
return value.Replace("\\\"", "\"").Replace("\\\\", "\\");
}
}
}
Loading