diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3ac28b5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,88 @@
+name: "CI"
+
+on:
+ push:
+ branches: [ develop, main ]
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ env:
+ Solution: "src/CleanMyPosts.sln"
+ UnitTest_Project: "src/UnitTests/UnitTests.csproj"
+ IntegrationTest_Project: "src/IntegrationTests/IntegrationTests.csproj"
+ FORCE_COLOR: "true"
+ DOTNET_LOGGING__CONSOLE__COLORBEHAVIOR: Enabled
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+
+ - name: Install .NET Tools (local)
+ run: |
+ dotnet new tool-manifest
+ dotnet tool install dotnet-reportgenerator-globaltool
+ dotnet tool install dotnet-sonarscanner
+
+ - name: Restore
+ run: dotnet restore "${{ env.Solution }}"
+
+ - name: Build and Test with SonarQube
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ dotnet tool run dotnet-sonarscanner begin `
+ /k:"thorstenalpers_CleanMyPosts" `
+ /o:"thorstenalpers" `
+ /d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
+ /d:sonar.host.url="https://sonarcloud.io" `
+ /d:sonar.sources="src" `
+ /d:sonar.tests="src/UnitTests;src/IntegrationTests" `
+ /d:sonar.test.inclusions="**/*Tests.cs" `
+ /d:sonar.coverageReportPaths="TestResults/Reports/SonarQube.xml"
+
+ dotnet build "${{ env.Solution }}" --configuration Release --no-restore
+
+ # Run Unit Tests
+ dotnet test "${{ env.UnitTest_Project }}" `
+ --collect:"XPlat Code Coverage" `
+ --results-directory TestResults/UnitTests `
+ --configuration Release `
+ --logger "console;verbosity=detailed" `
+ --filter "TestCategory!=Long-Running"
+
+ # Run Integration Tests
+ dotnet test "${{ env.IntegrationTest_Project }}" `
+ --collect:"XPlat Code Coverage" `
+ --results-directory TestResults/IntegrationTests `
+ --configuration Release `
+ --logger "console;verbosity=detailed"
+
+ # Generate coverage reports
+ dotnet tool run reportgenerator `
+ -reports:TestResults/**/coverage.cobertura.xml `
+ -targetdir:TestResults/Reports `
+ -reporttypes:"Html;lcov;SonarQube;Cobertura" `
+
+ dotnet tool run dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
+
+ - name: Upload to Coveralls
+ uses: coverallsapp/github-action@v2
+ with:
+ path-to-lcov: TestResults/Reports/lcov.info
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+
+ - name: Upload Test Coverage Report
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-coverage-report
+ path: TestResults/Reports
diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml
new file mode 100644
index 0000000..ff381b9
--- /dev/null
+++ b/.github/workflows/deploy-release.yml
@@ -0,0 +1,142 @@
+name: "Deploy Release"
+
+on:
+ workflow_dispatch: # manual trigger
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ env:
+ Solution: "src/CleanMyPosts.sln"
+ UI_Project: "src/UI/UI.csproj"
+ UnitTest_Project: "src/UnitTests/UnitTests.csproj"
+ IntegrationTest_Project: "src/IntegrationTests/IntegrationTests.csproj"
+ Installer_Script: "installer/Installer.iss"
+ FORCE_COLOR: "true"
+ DOTNET_LOGGING__CONSOLE__COLORBEHAVIOR: Enabled
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Important to fetch full history for git tag and branches
+
+ - name: Install .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+
+ - name: Restore
+ run: dotnet restore "${{ env.Solution }}"
+
+ - name: Build
+ run: dotnet build "${{ env.Solution }}" --configuration Release --no-restore
+
+ - name: Run Unit Tests
+ run: dotnet test "${{ env.UnitTest_Project }}" --configuration Release --logger "console;verbosity=detailed" --filter "TestCategory!=Long-Running"
+
+ - name: Run Integration Tests
+ run: dotnet test "${{ env.IntegrationTest_Project }}" --configuration Release --logger "console;verbosity=detailed"
+
+ - name: Extract Version
+ id: get_version
+ shell: pwsh
+ run: |
+ $content = Get-Content "${{ env.UI_Project }}"
+ if ($content -match '(.+)') {
+ $version = $matches[1]
+ echo "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
+ } else {
+ throw "Version not found in project file"
+ }
+
+ - name: Publish Single EXE
+ run: |
+ dotnet publish "${{ env.UI_Project }}" -c Release -r win-x64 --self-contained true `
+ /p:PublishSingleFile=true `
+ /p:IncludeAllContentForSelfExtract=true `
+ /p:EnableCompressionInSingleFile=true `
+ -o artifacts/single-exe
+
+ - name: Install Inno Setup
+ run: choco install innosetup --yes
+
+ - name: Build Setup EXE
+ run: |
+ iscc "/DMyAppVersion=${{ env.VERSION }}" "/DMyAppExePath=artifacts\\single-exe\\*" "${{ env.Installer_Script }}"
+
+ - name: Copy Setup EXE to Artifacts
+ run: |
+ mkdir -p artifacts/setup
+ copy installer\Output\CleanMyPosts-Setup-${{ env.VERSION }}.exe artifacts\setup\
+
+ - name: Rename Standalone EXE for Release
+ run: |
+ Rename-Item "artifacts/single-exe/CleanMyPosts.exe" "artifacts/single-exe/CleanMyPosts-standalone.exe"
+
+ - name: Generate update.xml
+ shell: pwsh
+ run: |
+ $version = "${{ env.VERSION }}"
+ $repo = "${{ github.repository }}"
+ $baseUrl = "https://github.com/$repo/releases/download/v$version"
+ $installerUrl = "$baseUrl/CleanMyPosts-Setup-$version.exe"
+ $changelogUrl = "https://github.com/$repo/releases/tag/v$version"
+ $xmlContent = @"
+
+
+
+ CleanMyPosts
+ $version
+ $installerUrl
+ $changelogUrl
+
+
+ "@
+ $xmlContent | Set-Content -Path "artifacts/update.xml" -Encoding UTF8
+
+ - name: Configure Git Credentials
+ run: |
+ echo "https://${{ secrets.GH_APIKEY }}@github.com" > $env:USERPROFILE\.git-credentials
+ git config --global credential.helper store
+ git config --global user.name "github-actions"
+ git config --global user.email "actions@github.com"
+
+ - name: Push update.xml to update-feed branch
+ run: |
+ git fetch origin update-feed || echo "No update-feed branch yet"
+ if git show-ref --verify --quiet refs/heads/update-feed; then
+ git checkout update-feed
+ else
+ git checkout --orphan update-feed
+ git rm -rf .
+ fi
+
+ cp artifacts/update.xml update.xml
+ git add update.xml
+
+ if git diff --cached --quiet; then
+ echo "No changes in update.xml; skipping commit"
+ else
+ git commit -m "Update appcast for version ${{ env.VERSION }}"
+ git push origin update-feed --force
+ fi
+
+ - name: Create Git Tag
+ run: |
+ git tag -a "v${{ env.VERSION }}" -m "Release v${{ env.VERSION }}"
+ git push origin "v${{ env.VERSION }}"
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: "v${{ env.VERSION }}"
+ name: "CleanMyPosts ${{ env.VERSION }}"
+ body_path: ./release-notes/v${{ env.VERSION }}.md
+ files: |
+ artifacts/single-exe/CleanMyPosts-standalone.exe
+ artifacts/setup/CleanMyPosts-Setup-${{ env.VERSION }}.exe
+ artifacts/update.xml
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index d240649..1b31fff 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,13 @@
-# CleanMyPosts
+
+
+
+[](#)
+[](./LICENSE.txt)
+[](https://sonarcloud.io/project/issues?issueStatuses=OPEN%2CCONFIRMED&id=thorstenalpers_CleanMyPosts)
+[](https://github.com/thorstenalpers/CleanMyPosts/actions/workflows/ci.yml)
+[](https://coveralls.io/github/thorstenalpers/CleanMyPosts?branch=develop)
+[](https://github.com/thorstenalpers/CleanMyPosts)
+
> ⚠️ **Warning:** Development in progress – the application has not been released.
@@ -21,7 +30,7 @@
* bookmark bar, hideable via settings
-# 🧹 CleanMyPosts
+---
**CleanMyPosts** is a lightweight Windows desktop application that securely deletes all tweets from your X (formerly Twitter) account in bulk. Designed for privacy-focused users, social media managers, or anyone looking to start fresh.
diff --git a/installer/Installer.iss b/installer/Installer.iss
index 167cc16..acfb0bc 100644
--- a/installer/Installer.iss
+++ b/installer/Installer.iss
@@ -1,40 +1,32 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
#define MyAppName "CleanMyPosts"
-#define MyAppVersion "0.0.1"
#define MyAppPublisher "Thorsten Alpers"
#define MyAppURL "https://github.com/thorstenalpers/CleanMyPosts"
#define MyAppExeName "CleanMyPosts.exe"
#define MyIconPath "..\src\UI\Assets\logo.ico"
-#define MyAppExePath "..\src\UI\bin\Release\net9.0-windows10.0.19041.0\win-x64\publish\*"
+
+; dynamically set in github actions, ifndef use local values
+#ifndef MyAppVersion
+ #define MyAppVersion "0.0.1"
+#endif
+#ifndef MyAppExePath
+ #define MyAppExePath "..\src\UI\bin\Release\net9.0-windows10.0.19041.0\win-x64\publish\*"
+#endif
[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{AEE32610-58A5-4785-98B0-B651865B30D2}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
-;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
-; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
-; on anything but x64 and Windows 11 on Arm.
ArchitecturesAllowed=x64compatible
-; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
-; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
-; meaning it should use the native 64-bit Program Files directory and
-; the 64-bit view of the registry.
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only).
PrivilegesRequired=admin
-; PrivilegesRequiredOverridesAllowed=no
-OutputBaseFilename=CleanMyPosts_Setup_{#MyAppVersion}
+OutputBaseFilename=CleanMyPosts-Setup-{#MyAppVersion}
SolidCompression=yes
WizardStyle=modern
SetupIconFile={#MyIconPath}
@@ -49,13 +41,10 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
Source: "{#MyAppExePath}"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyIconPath}"; DestDir: "{app}"; Flags: ignoreversion
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\logo.ico"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\logo.ico"; Tasks: desktopicon
-
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
diff --git a/release-notes/v0.0.1.md b/release-notes/v0.0.1.md
new file mode 100644
index 0000000..8bf8e03
--- /dev/null
+++ b/release-notes/v0.0.1.md
@@ -0,0 +1,3 @@
+### What's Changed
+
+* First Release
diff --git a/src/CleanMyPosts.sln b/src/CleanMyPosts.sln
index e2176a9..31cbeab 100644
--- a/src/CleanMyPosts.sln
+++ b/src/CleanMyPosts.sln
@@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI", "UI\UI.csproj", "{48A1
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{A65F1C77-11C1-DC4F-0A69-6EE789D29685}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject
@@ -19,6 +19,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\README.md = ..\README.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{59FDAA84-6701-32D0-9E5C-CA30D0B3B987}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+ ProjectSection(SolutionItems) = preProject
+ ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml
+ ..\.github\workflows\deploy-release.yml = ..\.github\workflows\deploy-release.yml
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -37,10 +45,17 @@ Global
{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7B3E70B-3487-8FDC-C91B-9CA0F0F66BE0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59FDAA84-6701-32D0-9E5C-CA30D0B3B987}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5A00240-64B9-4718-9682-317A36915078}
EndGlobalSection
diff --git a/src/IntegrationTests/IntegrationTests.csproj b/src/IntegrationTests/IntegrationTests.csproj
new file mode 100644
index 0000000..31e1348
--- /dev/null
+++ b/src/IntegrationTests/IntegrationTests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net9.0-windows10.0.19041.0
+ false
+ uap10.0.18362
+ x64;x86;AnyCPU
+ CleanMyPosts.IntegrationTests
+ CleanMyPosts.IntegrationTests
+ CleanMyPosts.IntegrationTests
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/IntegrationTests/PagesTests.cs b/src/IntegrationTests/PagesTests.cs
new file mode 100644
index 0000000..9269c3f
--- /dev/null
+++ b/src/IntegrationTests/PagesTests.cs
@@ -0,0 +1,53 @@
+using CleanMyPosts.UI.Contracts.Services;
+using CleanMyPosts.UI.ViewModels;
+using CleanMyPosts.UI.Views;
+using Microsoft.Extensions.Hosting;
+using NUnit.Framework;
+
+namespace CleanMyPosts.IntegrationTests;
+
+[Category("Integration")]
+public class PagesTests
+{
+ private IHost _host;
+
+ [SetUp]
+ public void Setup()
+ {
+ _host = TestHelper.SetUpHost();
+ }
+
+ [Test]
+ public void TestWebViewViewModelCreation()
+ {
+ var vm = _host.Services.GetService(typeof(XViewModel));
+ Assert.That(vm, Is.Not.Null);
+ }
+
+ [Test]
+ public void TestGetMainPageType()
+ {
+ var pageService = _host.Services.GetService(typeof(IPageService)) as IPageService;
+ Assert.That(pageService, Is.Not.Null);
+
+ var pageType = pageService.GetPageType(typeof(XViewModel).FullName);
+ Assert.That(typeof(XPage), Is.EqualTo(pageType));
+ }
+
+ [Test]
+ public void TestSettingsViewModelCreation()
+ {
+ var vm = _host.Services.GetService(typeof(SettingsViewModel));
+ Assert.That(vm, Is.Not.Null);
+ }
+
+ [Test]
+ public void TestGetSettingsPageType()
+ {
+ var pageService = _host.Services.GetService(typeof(IPageService)) as IPageService;
+ Assert.That(pageService, Is.Not.Null);
+
+ var pageType = pageService.GetPageType(typeof(SettingsViewModel).FullName);
+ Assert.That(typeof(SettingsPage), Is.EqualTo(pageType));
+ }
+}
diff --git a/src/IntegrationTests/TestHelper.cs b/src/IntegrationTests/TestHelper.cs
new file mode 100644
index 0000000..6ab724f
--- /dev/null
+++ b/src/IntegrationTests/TestHelper.cs
@@ -0,0 +1,64 @@
+using CleanMyPosts.UI.Helpers;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NetSparkleUpdater.Interfaces;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace CleanMyPosts.IntegrationTests;
+
+internal static class TestHelper
+{
+ public static IHost SetUpHost()
+ {
+ JsonConvert.DefaultSettings = () => new JsonSerializerSettings
+ {
+ Converters = { new StringEnumConverter() },
+ Formatting = Formatting.Indented
+ };
+
+ var cfgBuilder = new ConfigurationBuilder();
+ cfgBuilder.AddInMemoryCollection(new Dictionary
+ {
+ ["Updater:AppCastUrl"] = "https://example.com/appcast.xml",
+ ["Updater:SecurityMode"] = "Unsafe",
+ ["Updater:IconUri"] = "https://raw.githubusercontent.com/thorstenalpers/CleanMyPosts/refs/heads/main/src/UI/Assets/logo.ico"
+ });
+ cfgBuilder.AddUserSecrets();
+ cfgBuilder.AddEnvironmentVariables();
+ var cfg = cfgBuilder.Build();
+
+ var host = Host.CreateDefaultBuilder()
+ .ConfigureAppConfiguration((context, config) =>
+ {
+ config.AddConfiguration(cfg);
+ })
+ .ConfigureServices((context, services) =>
+ {
+ services.AddCleanMyPosts(cfg);
+ var mockUIFactory = new Mock();
+ services.AddSingleton(mockUIFactory.Object);
+ })
+ .ConfigureLogging(logging =>
+ {
+ logging.ClearProviders();
+ logging.AddConsole();
+ logging.SetMinimumLevel(LogLevel.Information);
+ logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning);
+ logging.AddFilter("CleanMyPosts", LogLevel.Information);
+
+ logging.AddSimpleConsole(options =>
+ {
+ options.UseUtcTimestamp = true;
+ options.SingleLine = true;
+ options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled;
+ });
+ })
+ .Build();
+
+ return host;
+ }
+}
diff --git a/src/Tests/PagesTests.cs b/src/Tests/PagesTests.cs
deleted file mode 100644
index 46bcb58..0000000
--- a/src/Tests/PagesTests.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System.Reflection;
-using CleanMyPosts.Core.Contracts.Services;
-using CleanMyPosts.Core.Services;
-using CleanMyPosts.UI.Contracts.Services;
-using CleanMyPosts.UI.Models;
-using CleanMyPosts.UI.Services;
-using CleanMyPosts.UI.ViewModels;
-using CleanMyPosts.UI.Views;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using NUnit.Framework;
-
-namespace CleanMyPosts.Tests;
-
-[Category("Unit")]
-public class PagesTests
-{
- private IHost _host;
-
- [SetUp]
- public void Setup()
- {
- var appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
- _host = Host.CreateDefaultBuilder()
- .ConfigureAppConfiguration(c => c.SetBasePath(appLocation))
- .ConfigureServices(ConfigureServices)
- .Build();
- }
-
- private void ConfigureServices(HostBuilderContext context, IServiceCollection services)
- {
- // Core Services
- services.AddSingleton();
-
- // Services
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- // ViewModels
- services.AddTransient();
- services.AddTransient();
-
- // Configuration
- services.Configure(context.Configuration.GetSection(nameof(AppConfig)));
- }
-
- [Test]
- public void TestWebViewViewModelCreation()
- {
- var vm = _host.Services.GetService(typeof(XViewModel));
- Assert.That(vm, Is.Not.Null);
- }
-
- [Test]
- public void TestGetMainPageType()
- {
- if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService)
- {
- var pageType = pageService.GetPageType(typeof(XViewModel).FullName);
- Assert.That(typeof(XPage), Is.EqualTo(pageType));
- }
- else
- {
- Assert.Fail($"Can't resolve {nameof(IPageService)}");
- }
- }
-
- [Test]
- public void TestSettingsViewModelCreation()
- {
- var vm = _host.Services.GetService(typeof(SettingsViewModel));
- Assert.That(vm, Is.Not.Null);
- }
-
- [Test]
- public void TestGetSettingsPageType()
- {
- if (_host.Services.GetService(typeof(IPageService)) is IPageService pageService)
- {
- var pageType = pageService.GetPageType(typeof(SettingsViewModel).FullName);
- Assert.That(typeof(SettingsPage), Is.EqualTo(pageType));
- }
- else
- {
- Assert.Fail($"Can't resolve {nameof(IPageService)}");
- }
- }
-}
diff --git a/src/UI/App.xaml.cs b/src/UI/App.xaml.cs
index 3ad67c4..fbd7f2a 100644
--- a/src/UI/App.xaml.cs
+++ b/src/UI/App.xaml.cs
@@ -1,14 +1,8 @@
using System.Windows;
using System.Windows.Threading;
-using CleanMyPosts.Core.Contracts.Services;
-using CleanMyPosts.Core.Services;
-using CleanMyPosts.UI.Contracts.Services;
-using CleanMyPosts.UI.Contracts.Views;
+using CleanMyPosts.Core.Exception;
using CleanMyPosts.UI.Helpers;
-using CleanMyPosts.UI.Models;
-using CleanMyPosts.UI.Services;
using CleanMyPosts.UI.ViewModels;
-using CleanMyPosts.UI.Views;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -52,13 +46,12 @@ private async void OnStartup(object sender, StartupEventArgs e)
})
.Build();
- // 🔧 LogViewModel must be resolved **after** the host is built
var logViewModel = _host.Services.GetRequiredService();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(config)
.WriteTo.Debug()
- .WriteTo.LogViewModelSink(logViewModel) // custom sink
+ .WriteTo.LogViewModelSink(logViewModel)
.CreateLogger();
await _host.StartAsync();
@@ -69,45 +62,13 @@ private async void OnStartup(object sender, StartupEventArgs e)
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed.");
- throw;
+ throw new CleanMyPostsException("Application start-up failed.", ex);
}
}
internal static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
- // Your existing registrations
- services.AddHostedService();
-
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddTransient();
- services.AddTransient();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddTransient();
- services.AddTransient();
-
- services.AddTransient();
- services.AddTransient();
-
- services.AddHttpClient();
-
- services.Configure(context.Configuration.GetSection(nameof(AppConfig)));
- services.Configure(context.Configuration.GetSection("UpdateSettings"));
+ services.AddCleanMyPosts(context.Configuration);
}
private async void OnExit(object sender, ExitEventArgs e)
@@ -116,7 +77,7 @@ private async void OnExit(object sender, ExitEventArgs e)
logger.LogInformation("Application is stopping.");
await _host.StopAsync();
_host.Dispose();
- Log.CloseAndFlush();
+ await Log.CloseAndFlushAsync();
_host = null;
}
diff --git a/src/UI/AssemblyInfo.cs b/src/UI/AssemblyInfo.cs
new file mode 100644
index 0000000..2aa049f
--- /dev/null
+++ b/src/UI/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("CleanMyPosts.UnitTests")]
+[assembly: InternalsVisibleTo("CleanMyPosts.IntegrationTests")]
+
+// Moq: ILogger needs it to mock internal classes
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/UI/Assets/banner.png b/src/UI/Assets/banner.png
new file mode 100644
index 0000000..afbdec3
Binary files /dev/null and b/src/UI/Assets/banner.png differ
diff --git a/src/UI/Converters/EnumToBooleanConverter.cs b/src/UI/Converters/EnumToBooleanConverter.cs
index 38df352..d049659 100644
--- a/src/UI/Converters/EnumToBooleanConverter.cs
+++ b/src/UI/Converters/EnumToBooleanConverter.cs
@@ -9,26 +9,16 @@ public class EnumToBooleanConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- if (parameter is string enumString)
+ if (parameter is string enumString && Enum.IsDefined(EnumType, value))
{
- if (Enum.IsDefined(EnumType, value))
- {
- var enumValue = Enum.Parse(EnumType, enumString);
-
- return enumValue.Equals(value);
- }
+ var enumValue = Enum.Parse(EnumType, enumString);
+ return enumValue.Equals(value);
}
-
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
- if (parameter is string enumString)
- {
- return Enum.Parse(EnumType, enumString);
- }
-
- return null;
+ return parameter is string enumString ? Enum.Parse(EnumType, enumString) : null;
}
}
diff --git a/src/UI/Helpers/Helper.cs b/src/UI/Helpers/Helper.cs
index 6f7d081..a18137d 100644
--- a/src/UI/Helpers/Helper.cs
+++ b/src/UI/Helpers/Helper.cs
@@ -3,6 +3,6 @@ public static class Helper
{
public static string CleanJsonResult(string json)
{
- return json?.Replace("\\\"", "\"")?.Trim('\"') ?? "";
+ return json.Replace("\\\"", "\"").Trim('\"');
}
}
diff --git a/src/UI/Helpers/ServiceCollectionExtensions.cs b/src/UI/Helpers/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..d9b8106
--- /dev/null
+++ b/src/UI/Helpers/ServiceCollectionExtensions.cs
@@ -0,0 +1,68 @@
+using System.Windows;
+using System.Windows.Media.Imaging;
+using CleanMyPosts.Core.Contracts.Services;
+using CleanMyPosts.Core.Services;
+using CleanMyPosts.UI.Contracts.Services;
+using CleanMyPosts.UI.Contracts.Views;
+using CleanMyPosts.UI.Models;
+using CleanMyPosts.UI.Services;
+using CleanMyPosts.UI.ViewModels;
+using CleanMyPosts.UI.Views;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using NetSparkleUpdater.Interfaces;
+using NetSparkleUpdater.UI.WPF;
+
+namespace CleanMyPosts.UI.Helpers;
+
+public static class ServiceCollectionExtensions
+{
+ public static void AddCleanMyPosts(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddHostedService();
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp =>
+ {
+ UIFactory factory = null;
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ var uri = new Uri(options.IconUri, UriKind.Absolute);
+ var imageSource = new BitmapImage(uri);
+ factory = new UIFactory(imageSource);
+ });
+ return factory;
+ });
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddTransient();
+ services.AddTransient();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddHttpClient();
+
+ services.Configure(configuration.GetSection(nameof(AppConfig)));
+ services.Configure(configuration.GetSection("Updater"));
+ }
+}
diff --git a/src/UI/Models/AppConfig.cs b/src/UI/Models/AppConfig.cs
index 7fa747f..8c8479e 100644
--- a/src/UI/Models/AppConfig.cs
+++ b/src/UI/Models/AppConfig.cs
@@ -4,4 +4,5 @@ public class AppConfig
{
public string ConfigurationsFolder { get; set; }
public string AppPropertiesFileName { get; set; }
+ public string XBaseUrl { get; set; }
}
diff --git a/src/UI/Models/UpdaterOptions.cs b/src/UI/Models/UpdaterOptions.cs
index bb6f7cf..46434d6 100644
--- a/src/UI/Models/UpdaterOptions.cs
+++ b/src/UI/Models/UpdaterOptions.cs
@@ -1,7 +1,10 @@
-namespace CleanMyPosts.UI.Models;
+using NetSparkleUpdater.Enums;
+
+namespace CleanMyPosts.UI.Models;
public class UpdaterOptions
{
public string AppCastUrl { get; set; }
- public string SecurityMode { get; set; }
+ public SecurityMode SecurityMode { get; set; }
+ public string IconUri { get; set; }
}
\ No newline at end of file
diff --git a/src/UI/Properties/Resources.Designer.cs b/src/UI/Properties/Resources.Designer.cs
index e487eca..c92d32b 100644
--- a/src/UI/Properties/Resources.Designer.cs
+++ b/src/UI/Properties/Resources.Designer.cs
@@ -61,7 +61,7 @@ internal Resources() {
}
///
- /// Looks up a localized string similar to Clean My Posts.
+ /// Looks up a localized string similar to CleanMyPosts.
///
public static string AppDisplayName {
get {
diff --git a/src/UI/Properties/Resources.resx b/src/UI/Properties/Resources.resx
index 39e52fd..58c7ea8 100644
--- a/src/UI/Properties/Resources.resx
+++ b/src/UI/Properties/Resources.resx
@@ -172,7 +172,7 @@
Open or close navigation
- Clean My Posts
+ CleanMyPosts
Log
diff --git a/src/UI/Services/ApplicationHostService.cs b/src/UI/Services/ApplicationHostService.cs
index f0a6f4b..d0fe5ef 100644
--- a/src/UI/Services/ApplicationHostService.cs
+++ b/src/UI/Services/ApplicationHostService.cs
@@ -1,8 +1,8 @@
-using Microsoft.Extensions.Hosting;
-using CleanMyPosts.UI.Contracts.Activation;
+using CleanMyPosts.UI.Contracts.Activation;
using CleanMyPosts.UI.Contracts.Services;
using CleanMyPosts.UI.Contracts.Views;
using CleanMyPosts.UI.ViewModels;
+using Microsoft.Extensions.Hosting;
namespace CleanMyPosts.UI.Services;
@@ -17,7 +17,6 @@ public class ApplicationHostService(IServiceProvider serviceProvider,
private readonly IPersistAndRestoreService _persistAndRestoreService = persistAndRestoreService;
private readonly IThemeSelectorService _themeSelectorService = themeSelectorService;
private readonly IEnumerable _activationHandlers = activationHandlers;
- private IShellWindow _shellWindow;
private bool _isInitialized;
public async Task StartAsync(CancellationToken cancellationToken)
@@ -65,9 +64,9 @@ private async Task HandleActivationAsync()
if (!System.Windows.Application.Current.Windows.OfType().Any())
{
- _shellWindow = _serviceProvider.GetService(typeof(IShellWindow)) as IShellWindow;
- _navigationService.Initialize(_shellWindow.GetNavigationFrame());
- _shellWindow.ShowWindow();
+ var shellWindow = _serviceProvider.GetService(typeof(IShellWindow)) as IShellWindow;
+ _navigationService.Initialize(shellWindow.GetNavigationFrame());
+ shellWindow.ShowWindow();
_navigationService.NavigateTo(typeof(XViewModel).FullName);
await Task.CompletedTask;
}
diff --git a/src/UI/Services/UpdateService.cs b/src/UI/Services/UpdateService.cs
index 1c75cca..f887b50 100644
--- a/src/UI/Services/UpdateService.cs
+++ b/src/UI/Services/UpdateService.cs
@@ -1,29 +1,29 @@
-using System.Windows.Media.Imaging;
+using Ardalis.GuardClauses;
using CleanMyPosts.UI.Contracts.Services;
using CleanMyPosts.UI.Models;
using Microsoft.Extensions.Options;
using NetSparkleUpdater;
-using NetSparkleUpdater.Enums;
+using NetSparkleUpdater.Interfaces;
using NetSparkleUpdater.SignatureVerifiers;
-using NetSparkleUpdater.UI.WPF;
namespace CleanMyPosts.UI.Services;
public class UpdateService : IUpdateService
{
private readonly SparkleUpdater _sparkle;
- public UpdateService(IOptions options)
- {
- var securityMode = options.Value.SecurityMode == "Strict" ? SecurityMode.Strict : SecurityMode.Unsafe;
- var uri = new Uri("pack://application:,,,/CleanMyPosts;component/Assets/logo.ico", UriKind.Absolute);
- var imageSource = new BitmapImage(uri);
+ public UpdateService(IOptions options, IUIFactory uIFactory)
+ {
+ var opts = options.Value;
+ Guard.Against.Null(opts);
+ Guard.Against.NullOrWhiteSpace(opts.AppCastUrl);
+ Guard.Against.NullOrWhiteSpace(opts.SecurityMode.ToString());
- var verifier = new DSAChecker(securityMode);
- _sparkle = new SparkleUpdater(options.Value.AppCastUrl, verifier)
+ var verifier = new DSAChecker(opts.SecurityMode);
+ _sparkle = new SparkleUpdater(opts.AppCastUrl, verifier)
{
- UIFactory = new UIFactory(imageSource),
+ UIFactory = uIFactory,
RelaunchAfterUpdate = true,
UseNotificationToast = true
};
diff --git a/src/UI/Services/WindowManagerService.cs b/src/UI/Services/WindowManagerService.cs
index af313f8..e4ec40e 100644
--- a/src/UI/Services/WindowManagerService.cs
+++ b/src/UI/Services/WindowManagerService.cs
@@ -15,9 +15,9 @@ public class WindowManagerService(IServiceProvider serviceProvider, IPageService
private readonly IPageService _pageService = pageService;
public Window MainWindow => Application.Current.MainWindow;
- public void OpenInNewWindow(string key, object parameter = null)
+ public void OpenInNewWindow(string pageKey, object parameter = null)
{
- var existingWindow = GetWindow(key);
+ var existingWindow = GetWindow(pageKey);
if (existingWindow is not null)
{
existingWindow.Activate();
@@ -39,11 +39,11 @@ public void OpenInNewWindow(string key, object parameter = null)
frame.Navigated += OnNavigated;
newWindow.Closed += OnWindowClosed;
- frame.Navigate(_pageService.GetPage(key), parameter);
+ frame.Navigate(_pageService.GetPage(pageKey), parameter);
newWindow.Show();
}
- public bool? OpenInDialog(string key, object parameter = null)
+ public bool? OpenInDialog(string pageKey, object parameter = null)
{
if (_serviceProvider.GetService(typeof(IShellDialogWindow)) is not IShellDialogWindow shellWindow)
{
@@ -54,17 +54,17 @@ public void OpenInNewWindow(string key, object parameter = null)
frame.Navigated += OnNavigated;
((Window)shellWindow).Closed += OnWindowClosed;
- frame.Navigate(_pageService.GetPage(key), parameter);
+ frame.Navigate(_pageService.GetPage(pageKey), parameter);
return ((Window)shellWindow).ShowDialog();
}
- public Window GetWindow(string key)
+ public Window GetWindow(string pageKey)
{
foreach (Window window in Application.Current.Windows)
{
var dataContext = window.GetDataContext();
- if (dataContext?.GetType().FullName == key)
+ if (dataContext?.GetType().FullName == pageKey)
{
return window;
}
@@ -72,7 +72,7 @@ public Window GetWindow(string key)
return null;
}
- private void OnNavigated(object sender, NavigationEventArgs e)
+ private static void OnNavigated(object sender, NavigationEventArgs e)
{
if (sender is Frame frame &&
frame.GetDataContext() is INavigationAware navigationAware)
diff --git a/src/UI/Services/XWebViewScriptService.cs b/src/UI/Services/XWebViewScriptService.cs
index 02afb43..f9339ba 100644
--- a/src/UI/Services/XWebViewScriptService.cs
+++ b/src/UI/Services/XWebViewScriptService.cs
@@ -15,9 +15,8 @@ public class XWebViewScriptService(ILogger logger, IWebVi
public async Task ShowPostsAsync()
{
- Guard.Against.Null(_userName, nameof(_userName));
+ Guard.Against.Null(_userName);
- //var searchQuery = $"from:{_userName} since:2000-01-01";
var searchQuery = $"from:{_userName}";
var encodedQuery = WebUtility.UrlEncode(searchQuery);
var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
@@ -27,17 +26,15 @@ public async Task ShowPostsAsync()
_webViewHostService.Source = url;
if (!await WaitForFullDocumentReadyAsync())
{
- _logger.LogWarning("Navigation to {url} failed.", url);
- return;
+ _logger.LogWarning("Navigation to {Url} failed.", url);
}
}
}
public async Task DeletePostsAsync()
{
- Guard.Against.Null(_userName, nameof(_userName));
+ Guard.Against.Null(_userName);
- //var searchQuery = $"from:{_userName} since:2000-01-01";
var searchQuery = $"from:{_userName}";
var encodedQuery = WebUtility.UrlEncode(searchQuery);
var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
@@ -85,7 +82,7 @@ public async Task DeletePostsAsync()
public async Task ShowLikesAsync()
{
- Guard.Against.Null(_userName, nameof(_userName));
+ Guard.Against.Null(_userName);
var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/likes");
if (_webViewHostService.Source != url)
@@ -93,8 +90,7 @@ public async Task ShowLikesAsync()
_webViewHostService.Source = url;
if (!await WaitForFullDocumentReadyAsync())
{
- _logger.LogWarning("Navigation to {url} failed.", url);
- return;
+ _logger.LogWarning("Navigation to {Url} failed.", url);
}
}
}
@@ -106,7 +102,7 @@ public Task DeleteStarredAsync()
public async Task ShowFollowingAsync()
{
- Guard.Against.Null(_userName, nameof(_userName));
+ Guard.Against.Null(_userName);
var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/following");
if (_webViewHostService.Source != url)
@@ -114,8 +110,7 @@ public async Task ShowFollowingAsync()
_webViewHostService.Source = url;
if (!await WaitForFullDocumentReadyAsync())
{
- _logger.LogWarning("Navigation to {url} failed.", url);
- return;
+ _logger.LogWarning("Navigation to {Url} failed.", url);
}
}
}
diff --git a/src/UI/ViewModels/XViewModel.cs b/src/UI/ViewModels/XViewModel.cs
index 3a19a0e..d10a3dc 100644
--- a/src/UI/ViewModels/XViewModel.cs
+++ b/src/UI/ViewModels/XViewModel.cs
@@ -5,6 +5,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Microsoft.Web.WebView2.Wpf;
namespace CleanMyPosts.UI.ViewModels;
@@ -14,10 +15,9 @@ public partial class XViewModel : ObservableObject
private readonly IWebViewHostService _webViewHostService;
private readonly ILogger _logger;
private readonly IXWebViewScriptService _xWebViewScriptService;
- //private readonly IWindowManagerService _windowManagerService;
private OverlayWindow _overlayWindow;
- private const string XBaseUrl = "https://x.com";
+ private readonly string xBaseUrl;
[ObservableProperty]
private bool _areButtonsEnabled;
@@ -26,17 +26,16 @@ public partial class XViewModel : ObservableObject
private string _userName;
public XViewModel(ILogger logger,
- //IWindowManagerService windowManagerService,
IWebViewHostService webViewHostService,
+ IOptions options,
IXWebViewScriptService xWebViewScriptService)
{
_webViewHostService = webViewHostService ?? throw new ArgumentNullException(nameof(webViewHostService));
_xWebViewScriptService = xWebViewScriptService ?? throw new ArgumentNullException(nameof(xWebViewScriptService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- //_windowManagerService = windowManagerService ?? throw new ArgumentNullException(nameof(windowManagerService));
- _xWebViewScriptService = xWebViewScriptService ?? throw new ArgumentNullException(nameof(xWebViewScriptService));
_webViewHostService.NavigationCompleted += OnNavigationCompleted;
_webViewHostService.WebMessageReceived += OnWebMessageReceived;
+ xBaseUrl = options.Value.XBaseUrl;
}
public async Task InitializeAsync(WebView2 webView)
@@ -48,7 +47,7 @@ public async Task InitializeAsync(WebView2 webView)
await _webViewHostService.InitializeAsync(webView);
- _webViewHostService.Source = new Uri(XBaseUrl);
+ _webViewHostService.Source = new Uri(xBaseUrl);
var jsScript = @"
window.onerror = function(message, source, lineno, colno, error) {
@@ -163,8 +162,7 @@ private async Task ShowPosts()
private async Task DeletePosts()
{
EnableUserInteractions(false);
- await Task.Delay(10000);
- //await _xWebViewScriptService.DeleteAllPostsAsync(_webView);
+ await _xWebViewScriptService.DeletePostsAsync();
EnableUserInteractions(true);
}
@@ -180,8 +178,7 @@ private async Task ShowLikes()
private async Task DeleteLikes()
{
EnableUserInteractions(false);
- await Task.Delay(10000);
- //await _xWebViewScriptService.DeleteAllPostsAsync(_webView);
+ await Task.Delay(10002);
EnableUserInteractions(true);
}
@@ -197,8 +194,7 @@ private async Task ShowFollowing()
private async Task DeleteFollowing()
{
EnableUserInteractions(false);
- await Task.Delay(10000);
- //await _xWebViewScriptService.DeleteAllPostsAsync(_webView);
+ await Task.Delay(10001);
EnableUserInteractions(true);
}
diff --git a/src/UI/Views/OverlayWindow.xaml.cs b/src/UI/Views/OverlayWindow.xaml.cs
index 4555872..c7879d6 100644
--- a/src/UI/Views/OverlayWindow.xaml.cs
+++ b/src/UI/Views/OverlayWindow.xaml.cs
@@ -22,7 +22,7 @@ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
try
{
// Try dragging the main window instead
- ((Window)mainWindow).DragMove();
+ mainWindow.DragMove();
}
catch (InvalidOperationException)
{
diff --git a/src/UI/Views/ShellWindow.xaml b/src/UI/Views/ShellWindow.xaml
index 17809a0..e5f2cc1 100644
--- a/src/UI/Views/ShellWindow.xaml
+++ b/src/UI/Views/ShellWindow.xaml
@@ -4,9 +4,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:fa="http://metro.mahapps.com/winfx/xaml/iconpacks/fontawesome"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:material="http://metro.mahapps.com/winfx/xaml/iconpacks/material"
- xmlns:fa="http://metro.mahapps.com/winfx/xaml/iconpacks/fontawesome"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:properties="clr-namespace:CleanMyPosts.UI.Properties"
xmlns:templateSelectors="clr-namespace:CleanMyPosts.UI.TemplateSelectors"
diff --git a/src/UI/appsettings.json b/src/UI/appsettings.json
index 4b3fc62..ea35664 100644
--- a/src/UI/appsettings.json
+++ b/src/UI/appsettings.json
@@ -1,11 +1,13 @@
{
"AppConfig": {
"configurationsFolder": "CleanMyPosts\\Configurations",
- "appPropertiesFileName": "AppProperties.json"
+ "appPropertiesFileName": "AppProperties.json",
+ "XBaseUrl": "https://x.com"
},
"Updater": {
- "AppCastUrl": "https://raw.githubusercontent.com/youruser/yourrepo/main/AutoUpdater.xml",
- "SecurityMode": "Strict"
+ "AppCastUrl": "https://raw.githubusercontent.com/thorstenalpers/CleanMyPosts/refs/heads/update-feed/update.xml",
+ "SecurityMode": "Strict",
+ "IconUri": "pack://application:,,,/CleanMyPosts;component/Assets/logo.ico"
},
"Serilog": {
"MinimumLevel": {
diff --git a/src/Tests/SettingsViewModelTests.cs b/src/UnitTests/SettingsViewModelTests.cs
similarity index 97%
rename from src/Tests/SettingsViewModelTests.cs
rename to src/UnitTests/SettingsViewModelTests.cs
index b9c096f..5d7ffa5 100644
--- a/src/Tests/SettingsViewModelTests.cs
+++ b/src/UnitTests/SettingsViewModelTests.cs
@@ -5,7 +5,7 @@
using Moq;
using NUnit.Framework;
-namespace CleanMyPosts.Tests;
+namespace CleanMyPosts.UnitTests;
[Category("Unit")]
public class SettingsViewModelTests
@@ -40,7 +40,7 @@ public void TestSettingsViewModel_SetCurrentVersion()
Mock mockUpdateService = new();
Mock> mockLogger = new();
- Version testVersion = new(1, 2, 3, 4);
+ Version testVersion = new(1, 2, 3);
mockApplicationInfoService.Setup(mock => mock.GetVersion()).Returns(testVersion);
var settingsVm = new SettingsViewModel(mockThemeSelectorService.Object,
diff --git a/src/Tests/Tests.csproj b/src/UnitTests/UnitTests.csproj
similarity index 54%
rename from src/Tests/Tests.csproj
rename to src/UnitTests/UnitTests.csproj
index d62e402..7815e87 100644
--- a/src/Tests/Tests.csproj
+++ b/src/UnitTests/UnitTests.csproj
@@ -5,24 +5,20 @@
false
uap10.0.18362
x64;x86;AnyCPU
- CleanMyPosts.Tests
- CleanMyPosts.UI
+ CleanMyPosts.UnitTests
+ CleanMyPosts.UnitTests
enable
-
- x86
-
-
-
- x64
-
-
-
- AnyCPU
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+