diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml
index 5e0c2e6e1..2a66032aa 100644
--- a/.github/workflows/artifacts.yml
+++ b/.github/workflows/artifacts.yml
@@ -179,6 +179,11 @@ jobs:
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
+ # Build Windows launcher
+ if [ "${{ matrix.kind }}" == "windows" ]; then
+ dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
+ fi
+
# Pack files
if [ "${{ matrix.kind }}" == "windows" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8fa567b2..8ea9df9e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Jellyfin (web) and TiviMate (Android) were specifically tested
### Added
-- Hide console window on macOS; tray menu can be used to access UI, logs and to stop the app
+- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
- Also write logs to text files in the `logs` config subfolder
- Add `added_date` to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
diff --git a/ErsatzTV-Windows/ErsatzTV-Windows.csproj b/ErsatzTV-Windows/ErsatzTV-Windows.csproj
new file mode 100644
index 000000000..db3d6b6f6
--- /dev/null
+++ b/ErsatzTV-Windows/ErsatzTV-Windows.csproj
@@ -0,0 +1,33 @@
+
+
+
+ WinExe
+ net6.0-windows
+ ErsatzTV_Windows
+ enable
+ true
+ enable
+ Ersatztv.ico
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
\ No newline at end of file
diff --git a/ErsatzTV-Windows/Ersatztv.ico b/ErsatzTV-Windows/Ersatztv.ico
new file mode 100644
index 000000000..92c5a0383
Binary files /dev/null and b/ErsatzTV-Windows/Ersatztv.ico differ
diff --git a/ErsatzTV-Windows/Program.cs b/ErsatzTV-Windows/Program.cs
new file mode 100644
index 000000000..870cb8eb3
--- /dev/null
+++ b/ErsatzTV-Windows/Program.cs
@@ -0,0 +1,14 @@
+namespace ErsatzTV_Windows;
+
+public static class Program
+{
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ public static void Main()
+ {
+ ApplicationConfiguration.Initialize();
+ Application.Run(new TrayApplicationContext());
+ }
+}
diff --git a/ErsatzTV-Windows/TrayApplicationContext.cs b/ErsatzTV-Windows/TrayApplicationContext.cs
new file mode 100644
index 000000000..18a4e3fa6
--- /dev/null
+++ b/ErsatzTV-Windows/TrayApplicationContext.cs
@@ -0,0 +1,77 @@
+using ErsatzTV.Core;
+using System.Diagnostics;
+using Asmichi.ProcessManagement;
+using System.Reflection;
+
+namespace ErsatzTV_Windows;
+
+public class TrayApplicationContext : ApplicationContext
+{
+ private readonly NotifyIcon _trayIcon;
+ private readonly IChildProcess? _childProcess;
+
+ public TrayApplicationContext()
+ {
+ _trayIcon = new NotifyIcon
+ {
+ Icon = new Icon("./Ersatztv.ico"),
+ ContextMenuStrip = new ContextMenuStrip(),
+ Visible = true
+ };
+
+ AddMenuItem("Launch Web UI", LaunchWebUI);
+ AddMenuItem("Show Logs", ShowLogs);
+ _trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
+ AddMenuItem("Exit", Exit);
+
+ string folder = AppContext.BaseDirectory;
+ string exe = Path.Combine(folder, "ErsatzTV.exe");
+
+ if (File.Exists(exe))
+ {
+ var si = new ChildProcessStartInfo(exe);
+ _childProcess = ChildProcess.Start(si);
+ }
+ }
+
+ private void AddMenuItem(string name, EventHandler action)
+ {
+ var item = new ToolStripMenuItem(name);
+ item.Click += action;
+ _trayIcon.ContextMenuStrip.Items.Add(item);
+ }
+
+ private void LaunchWebUI(object? sender, EventArgs e)
+ {
+ var process = new Process();
+ process.StartInfo.UseShellExecute = true;
+ process.StartInfo.FileName = "http://localhost:8409";
+ process.Start();
+ }
+
+ private void ShowLogs(object? sender, EventArgs e)
+ {
+ if (!Directory.Exists(FileSystemLayout.LogsFolder))
+ {
+ Directory.CreateDirectory(FileSystemLayout.LogsFolder);
+ }
+
+ var process = new Process();
+ process.StartInfo.UseShellExecute = true;
+ process.StartInfo.FileName = FileSystemLayout.LogsFolder;
+ process.Start();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ _childProcess?.Dispose();
+ base.Dispose(disposing);
+ }
+
+ private void Exit(object? sender, EventArgs e)
+ {
+ // Hide tray icon, otherwise it will remain shown until user mouses over it
+ _trayIcon.Visible = false;
+ Application.Exit();
+ }
+}
diff --git a/ErsatzTV.Core/FileSystemLayout.cs b/ErsatzTV.Core/FileSystemLayout.cs
index a6f57870c..96b7ca67a 100644
--- a/ErsatzTV.Core/FileSystemLayout.cs
+++ b/ErsatzTV.Core/FileSystemLayout.cs
@@ -18,10 +18,12 @@ namespace ErsatzTV.Core
Environment.SpecialFolderOption.Create),
"etv-transcode");
+ public static readonly string LogsFolder = Path.Combine(AppDataFolder, "logs");
+
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
- public static readonly string LogFilePath = Path.Combine(AppDataFolder, "logs", "ersatztv.log");
+ public static readonly string LogFilePath = Path.Combine(LogsFolder, "ersatztv.log");
public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");
diff --git a/ErsatzTV.sln b/ErsatzTV.sln
index e9e62455a..a23602365 100644
--- a/ErsatzTV.sln
+++ b/ErsatzTV.sln
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Application", "Ers
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Core.Tests", "ErsatzTV.Core.Tests\ErsatzTV.Core.Tests.csproj", "{CE7F1ACD-F286-4761-A7BC-A541A1E25C86}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV-Windows", "ErsatzTV-Windows\ErsatzTV-Windows.csproj", "{805DB439-1CC1-44E1-A39F-F472635CF94E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,6 +38,10 @@ Global
{CE7F1ACD-F286-4761-A7BC-A541A1E25C86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE7F1ACD-F286-4761-A7BC-A541A1E25C86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE7F1ACD-F286-4761-A7BC-A541A1E25C86}.Release|Any CPU.Build.0 = Release|Any CPU
+ {805DB439-1CC1-44E1-A39F-F472635CF94E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {805DB439-1CC1-44E1-A39F-F472635CF94E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {805DB439-1CC1-44E1-A39F-F472635CF94E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {805DB439-1CC1-44E1-A39F-F472635CF94E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
diff --git a/artwork/Ersatztv.ico b/artwork/Ersatztv.ico
new file mode 100644
index 000000000..92c5a0383
Binary files /dev/null and b/artwork/Ersatztv.ico differ