Compare commits

...

11 commits

10 changed files with 257 additions and 53 deletions

14
.editorconfig Normal file
View file

@ -0,0 +1,14 @@
[*]
guidelines = 80
end_of_line = lf
insert_final_newline = true
charset = utf-8-bom
[*.cs]
guidelines = 80, 120
# IDE0021: Use block body for constructors
csharp_style_expression_bodied_constructors = when_on_single_line
# IDE0024: Use block body for operators
csharp_style_expression_bodied_operators = when_on_single_line

View file

@ -0,0 +1,33 @@
name: Create release on tag push
on:
push:
tags:
- 'release/*'
jobs:
build:
name: Create release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Get release body
run: |
echo "release_body=$(cat CHANGELOG.md)" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Draft release
uses: https://gitea.com/actions/release-action@main
with:
body: ${{ env.release_body }}
draft: true
api_key: '${{ secrets.RELEASE_TOKEN }}'

12
.github/dependabot.yaml vendored Normal file
View file

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/"
target-branch: "develop"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "develop"
schedule:
interval: "daily"

31
.github/workflows/create-release.yaml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Create release on tag push
on:
push:
tags:
- 'release/*'
jobs:
build:
name: Create release
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1
- name: Build
run: msbuild -t:build -p:configuration=release
- name: Draft release
uses: ncipollo/release-action@v1
with:
artifacts: "VoiceAttack-EliteScreenshots.zip"
bodyFile: "CHANGELOG.md"
draft: true
token: ${{ secrets.RELEASE_TOKEN }}

6
Directory.Build.targets Normal file
View file

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

21
Directory.build.props Normal file
View file

@ -0,0 +1,21 @@
<Project>
<!-- StyleCop Analyzers configuration -->
<PropertyGroup>
<CodeAnalysisRuleSet>$(SolutionDir)StyleCop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers.Error" Version="1.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.CSharp.Async.Rules" Version="6.1.41">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<AdditionalFiles Include="$(SolutionDir)stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,23 @@
#nullable enable
// <copyright file="EliteScreenshots.cs" company="alterNERDtive">
// Copyright 20212022 alterNERDtive.
//
// This file is part of VoiceAttack EliteScreenshots plugin.
//
// VoiceAttack EliteScreenshots plugin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// VoiceAttack EliteScreenshots plugin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with VoiceAttack EliteScreenshots plugin. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
#nullable enable
using System;
using System.Drawing;
@ -11,16 +30,23 @@ using System.Threading;
namespace EliteScreenshots
{
/// <summary>
/// VoiceAttack plugin that automatically detects, converts and moves
/// screenshots created by Elite Dangerous in the background.
/// </summary>
public class EliteScreenshots
{
private static dynamic? VA;
private static readonly string screenshotsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), @"Frontier Developments\Elite Dangerous");
private static readonly string defaultFormat = "%datetime%-%cmdr%-%system%-%body%";
private static readonly string defaultOutputDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
private static readonly string ScreenshotsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), @"Frontier Developments\Elite Dangerous");
private static readonly string DefaultFormat = "%datetime%-%cmdr%-%system%-%body%";
private static readonly string DefaultOutputDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
private static readonly Regex standardRegex = new Regex(@"^Screenshot_\d{4}\.bmp$");
private static readonly Regex highResRegex = new Regex(@"^HighResScreenShot_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.bmp$");
private static readonly Regex tokenRegex = new Regex(@"%(?<token>[\w: \-\.]*)%");
private static readonly Regex StandardRegex = new (@"^Screenshot_\d{4}\.bmp$");
private static readonly Regex HighResRegex = new (@"^HighResScreenShot_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.bmp$");
private static readonly Regex TokenRegex = new (@"%(?<token>[\w: \-\.]*)%");
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "just cause")]
private static dynamic? VA;
private static FileSystemWatcher? fileWatcher;
private static FileSystemWatcher FileWatcher
{
@ -28,36 +54,53 @@ namespace EliteScreenshots
{
if (fileWatcher == null)
{
fileWatcher = new FileSystemWatcher(screenshotsDirectory);
fileWatcher.Created += (source, EventArgs) => { FileChangedHandler(EventArgs); };
fileWatcher = new FileSystemWatcher(ScreenshotsDirectory);
fileWatcher.Created += (source, eventArgs) => { FileChangedHandler(eventArgs); };
}
return fileWatcher!;
}
}
private static FileSystemWatcher? fileWatcher;
public static string VERSION = "0.1";
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "nicer grouping")]
private static readonly Version VERSION = new ("0.1");
/// <summary>
/// The plugins display name, as required by the VoiceAttack plugin API.
/// </summary>
/// <returns>The display name.</returns>
public static string VA_DisplayName() => $"EliteScreenshots Plugin {VERSION}";
/// <summary>
/// The plugins description, as required by the VoiceAttack plugin API.
/// </summary>
/// <returns>The description.</returns>
public static string VA_DisplayInfo() => VA_DisplayName();
/// <summary>
/// The plugins GUID, as required by the VoiceAtatck plugin API.
/// </summary>
/// <returns>The GUID.</returns>
public static Guid VA_Id() => new Guid("{252490FD-2E6F-4703-900B-02ED98D717C2}");
/// <summary>
/// The Init method, as required by the VoiceAttack plugin API.
/// Runs when the plugin is initially loaded.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
public static void VA_Init1(dynamic vaProxy)
{
VA = vaProxy;
VA.TextVariableChanged += new Action<string, string, string, Guid?>(TextVariableChanged);
VA.SetText("EliteScreenshots.version", VERSION);
VA.SetText("EliteScreenshots.version", VERSION.ToString());
try
{
// inform about old shots
DirectoryInfo dirInfo = new DirectoryInfo(screenshotsDirectory);
int standardCount = dirInfo.GetFiles().Where(file => standardRegex.IsMatch(file.Name)).Count();
int highResCount = dirInfo.GetFiles().Where(file => highResRegex.IsMatch(file.Name)).Count();
DirectoryInfo dirInfo = new DirectoryInfo(ScreenshotsDirectory);
int standardCount = dirInfo.GetFiles().Where(file => StandardRegex.IsMatch(file.Name)).Count();
int highResCount = dirInfo.GetFiles().Where(file => HighResRegex.IsMatch(file.Name)).Count();
if (standardCount > 0 && highResCount > 0)
{
@ -87,6 +130,11 @@ namespace EliteScreenshots
}
}
/// <summary>
/// The Invoke method, as required by the VoiceAttack plugin API.
/// Runs whenever a plugin context is invoked.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
public static void VA_Invoke1(dynamic vaProxy)
{
VA = vaProxy;
@ -108,9 +156,24 @@ namespace EliteScreenshots
}
}
public static void VA_StopCommand() { }
/// <summary>
/// The Exit method, as required by the VoiceAttack plugin API.
/// Runs when VoiceAttack is shut down.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "required by VoiceAttack plugin API")]
public static void VA_Exit1(dynamic vaProxy)
{
}
public static void VA_Exit1(dynamic vaProxy) { }
/// <summary>
/// The StopCommand method, as required by the VoiceAttack plugin API.
/// Runs whenever all commands are stopped using the “Stop All Commands”
/// button or action.
/// </summary>
public static void VA_StopCommand()
{
}
private static void LogError(string message)
{
@ -126,15 +189,15 @@ namespace EliteScreenshots
{
VA!.WriteToLog($"WARN | EliteScreenshots: {message}", "yellow");
}
private static string getTargetFileName(bool highres = false, string? fromFile = null)
private static string GetTargetFileName(bool highres = false, string? fromFile = null)
{
StringBuilder sb = new StringBuilder(VA!.GetText("EliteScreenshots.format#") ?? defaultFormat);
MatchCollection matches = tokenRegex.Matches(sb.ToString());
StringBuilder sb = new StringBuilder(VA!.GetText("EliteScreenshots.format#") ?? DefaultFormat);
MatchCollection matches = TokenRegex.Matches(sb.ToString());
string token;
string value;
if (String.IsNullOrEmpty(fromFile))
if (string.IsNullOrEmpty(fromFile))
{
foreach (Match match in matches)
{
@ -183,8 +246,8 @@ namespace EliteScreenshots
sb.Replace(c, '_');
}
string outputDirectory = VA!.GetText("EliteScreenshots.outputDirectory#") ?? defaultOutputDirectory;
string targetFilename = Path.Combine(outputDirectory, $"{sb}{(highres ? "-highres" : "")}.png");
string outputDirectory = VA!.GetText("EliteScreenshots.outputDirectory#") ?? DefaultOutputDirectory;
string targetFilename = Path.Combine(outputDirectory, $"{sb}{(highres ? "-highres" : string.Empty)}.png");
if (File.Exists(targetFilename))
{
@ -193,9 +256,10 @@ namespace EliteScreenshots
string newFileName;
do
{
newFileName = Path.Combine(outputDirectory, $"{sb}{(highres ? "-highres" : "")}_{i:D4}.png");
newFileName = Path.Combine(outputDirectory, $"{sb}{(highres ? "-highres" : string.Empty)}_{i:D4}.png");
i++;
} while (File.Exists(newFileName));
}
while (File.Exists(newFileName));
targetFilename = newFileName;
}
@ -205,33 +269,36 @@ namespace EliteScreenshots
private static string ConvertAndMove(string source, string? target = null, bool highres = false)
{
target ??= getTargetFileName(highres);
target ??= GetTargetFileName(highres);
using (Bitmap bm = new Bitmap(source)) {
using (Bitmap bm = new Bitmap(source))
{
bm.Save(target, ImageFormat.Png);
}
LogInfo($"Saved{(highres ? " high resolution" : "")} screenshot to '{target}'.");
LogInfo($"Saved{(highres ? " high resolution" : string.Empty)} screenshot to '{target}'.");
File.Delete(source);
return target;
}
private static void ConvertOldShots ()
private static void ConvertOldShots()
{
DirectoryInfo dirInfo = new DirectoryInfo(screenshotsDirectory);
foreach (FileInfo fileInfo in dirInfo.GetFiles().Where(file => standardRegex.IsMatch(file.Name)).ToList())
DirectoryInfo dirInfo = new (ScreenshotsDirectory);
foreach (FileInfo fileInfo in dirInfo.GetFiles().Where(file => StandardRegex.IsMatch(file.Name)).ToList())
{
ConvertAndMove(fileInfo.FullName, target: getTargetFileName(fromFile: fileInfo.FullName));
ConvertAndMove(fileInfo.FullName, target: GetTargetFileName(fromFile: fileInfo.FullName));
}
foreach (FileInfo fileInfo in dirInfo.GetFiles().Where(file => highResRegex.IsMatch(file.Name)).ToList())
foreach (FileInfo fileInfo in dirInfo.GetFiles().Where(file => HighResRegex.IsMatch(file.Name)).ToList())
{
ConvertAndMove(fileInfo.FullName, target: getTargetFileName(fromFile: fileInfo.FullName, highres: true), highres: true);
ConvertAndMove(fileInfo.FullName, target: GetTargetFileName(fromFile: fileInfo.FullName, highres: true), highres: true);
}
}
public static void TextVariableChanged(string name, string from, string to, Guid? internalID)
private static void TextVariableChanged(string name, string from, string to, Guid? internalID = null)
{
if (name == "EliteScreenshots.format#")
{
@ -239,6 +306,7 @@ namespace EliteScreenshots
// (or just give example output along with it?)
LogInfo($"Output format changed to '{to}'.");
}
if (name == "EliteScreenshots.outputDirectory#")
{
// FIXXME check if it exists
@ -251,18 +319,18 @@ namespace EliteScreenshots
string name = eventArgs.Name;
try
{
if (standardRegex.IsMatch(name))
if (StandardRegex.IsMatch(name))
{
ConvertAndMove(Path.Combine(screenshotsDirectory, name));
ConvertAndMove(Path.Combine(ScreenshotsDirectory, name));
}
else if (highResRegex.IsMatch(name))
else if (HighResRegex.IsMatch(name))
{
// This is ugly AF …
// But I have to wait for Elite to finish writing, and no,
// I have not been able to find a viable alternative to this
// that would be less ugly.
Thread.Sleep(5000);
ConvertAndMove(Path.Combine(screenshotsDirectory, name), highres: true);
ConvertAndMove(Path.Combine(ScreenshotsDirectory, name), highres: true);
}
else
{

View file

@ -21,7 +21,6 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -30,7 +29,6 @@
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
@ -63,10 +61,7 @@
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>powershell if (Test-Path '$(SolutionDir)VoiceAttack-EliteScreenshots.zip') { Remove-Item -Path '$(SolutionDir)VoiceAttack-EliteScreenshots.zip' }</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>if $(ConfigurationName) == Release (powershell Compress-Archive -Path '$(TargetDir)' -DestinationPath '$(SolutionDir)VoiceAttack-EliteScreenshots.zip' -Force)</PostBuildEvent>
</PropertyGroup>
</Project>
<Target Name="AfterBuild" Condition=" '$(Configuration)' == 'Release' ">
<ZipDirectory SourceDirectory="bin" DestinationFile="VoiceAttack-EliteScreenshots.zip" Overwrite="true" />
</Target>
</Project>

View file

@ -1,10 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
# Visual Studio Version 17
VisualStudioVersion = 17.3.32519.111
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoiceAttack-EliteScreenshots", "VoiceAttack-EliteScreenshots.csproj", "{8EA92CFE-63FE-4C22-8F04-CD675FDCE0D1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AD6B93C4-D07C-4455-A26D-41369319C6A3}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\create-release.yaml = .github\workflows\create-release.yaml
Directory.build.props = Directory.build.props
stylecop.json = stylecop.json
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

16
stylecop.json Normal file
View file

@ -0,0 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace"
},
"documentationRules": {
"companyName": "alterNERDtive",
"copyrightText": "Copyright {year} {companyName}.\n\nThis file is part of {application}.\n\n{application} is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\n{application} is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with {application}. If not, see <https://www.gnu.org/licenses/>.",
"variables": {
"application": "VoiceAttack EliteScreenshots plugin",
"year": "20212022"
}
}
}
}