diff --git a/.github/workflows/build-windows-exe.yml b/.github/workflows/build-windows-exe.yml
index cd27c45..b6facaf 100644
--- a/.github/workflows/build-windows-exe.yml
+++ b/.github/workflows/build-windows-exe.yml
@@ -63,6 +63,16 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build lightweight instant-launch bootstrapper
+ shell: powershell
+ run: |
+ $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1
+ if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." }
+ & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ Copy-Item release\bootstrapper\SOCDOF-Installer.exe release\SOCDOF-Installer.exe -Force
+ if (-not (Test-Path release\SOCDOF-Installer.exe)) { throw "SOCDOF-Installer.exe was not produced." }
+
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
diff --git a/.github/workflows/installer-bootstrapper-framework-ci.yml b/.github/workflows/installer-bootstrapper-framework-ci.yml
new file mode 100644
index 0000000..bc03c6e
--- /dev/null
+++ b/.github/workflows/installer-bootstrapper-framework-ci.yml
@@ -0,0 +1,23 @@
+name: Validate SOCDOF Installer Bootstrapper (.NET Framework)
+
+on:
+ pull_request:
+ branches: [main, master]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build native-startup Windows bootstrapper
+ shell: powershell
+ run: |
+ $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1
+ if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." }
+ & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if (-not (Test-Path release\bootstrapper\SOCDOF-Installer.exe)) { throw "Bootstrapper executable was not produced." }
diff --git a/.github/workflows/socdof-installer-validation.yml b/.github/workflows/socdof-installer-validation.yml
new file mode 100644
index 0000000..f7c1307
--- /dev/null
+++ b/.github/workflows/socdof-installer-validation.yml
@@ -0,0 +1,74 @@
+name: Validate SOCDOF App and Installer
+
+on:
+ pull_request:
+ branches: [main, master]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ runs-on: windows-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Install dependencies
+ run: npm install --no-package-lock
+
+ - name: TypeScript validation
+ run: npm run lint
+
+ - name: Build web application
+ run: npm run build
+
+ - name: Prepare NSIS artwork
+ run: node scripts/generate_nsis_bitmaps.js
+
+ - name: Install Electron packaging tools
+ run: npm install --no-save --no-package-lock electron electron-builder
+
+ - name: Package Windows NSIS installer
+ run: npx --yes electron-builder --win --x64 --config electron-builder.json --publish never
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Verify Windows installer artifact
+ shell: powershell
+ run: |
+ $setup = Get-ChildItem release -Filter 'SOCDOF-Setup-*.exe' -File | Select-Object -First 1
+ if (-not $setup) { throw "Electron Builder did not produce the expected SOCDOF-Setup-*.exe artifact." }
+ Write-Host "Verified installer artifact: $($setup.FullName) ($($setup.Length) bytes)"
+
+ - name: Smoke-test fresh install and in-place upgrade
+ shell: powershell
+ run: |
+ $setup = Get-ChildItem release -Filter 'SOCDOF-Setup-*.exe' -File | Select-Object -First 1
+ if (-not $setup) { throw "No SOCDOF NSIS installer is available for smoke testing." }
+ $installPath = Join-Path $env:RUNNER_TEMP 'SOCDOF-SmokeInstall'
+ $firstInstall = Start-Process -FilePath $setup.FullName -ArgumentList @('/S', "/D=$installPath") -Wait -PassThru
+ if ($firstInstall.ExitCode -ne 0) { throw "Fresh install failed with exit code $($firstInstall.ExitCode)." }
+ $appExe = Join-Path $installPath 'SOCDOF.exe'
+ if (-not (Test-Path $appExe)) { throw "Fresh install did not produce SOCDOF.exe at the selected install path." }
+ Get-Process -Name 'SOCDOF' -ErrorAction SilentlyContinue | Stop-Process -Force
+ $upgrade = Start-Process -FilePath $setup.FullName -ArgumentList @('/S', "/D=$installPath") -Wait -PassThru
+ if ($upgrade.ExitCode -ne 0) { throw "In-place upgrade failed with exit code $($upgrade.ExitCode)." }
+ if (-not (Test-Path $appExe)) { throw "In-place upgrade removed or failed to preserve SOCDOF.exe." }
+ Get-Process -Name 'SOCDOF' -ErrorAction SilentlyContinue | Stop-Process -Force
+ Write-Host "Fresh install and in-place upgrade passed at $installPath."
+
+ - name: Compile instant-launch bootstrapper
+ shell: powershell
+ run: |
+ $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1
+ if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." }
+ & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if (-not (Test-Path release\bootstrapper\SOCDOF-Installer.exe)) { throw "Bootstrapper executable was not produced." }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85208f5..9f600e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,3 +6,8 @@ Format guidelines:
- Keep it concise: what is new, what changed/improved, what was fixed.
- Avoid exhaustive UI/visual layout breakdowns.
-->
+
+- **Changed:** Windows setup now allows choosing the installation directory while defaulting to a per-user Local AppData location.
+- **Improved:** Added a minimal branded startup splash while the SOCDOF desktop workspace initializes.
+
+- **Added:** A lightweight Windows bootstrapper opens immediately, downloads the latest setup with real progress, and lets users select the installation directory.
diff --git a/electron-builder.json b/electron-builder.json
index 4e325ba..5b5f1eb 100644
--- a/electron-builder.json
+++ b/electron-builder.json
@@ -16,7 +16,9 @@
{
"from": "languages",
"to": "languages",
- "filter": ["**/*"]
+ "filter": [
+ "**/*"
+ ]
}
],
"win": {
@@ -31,9 +33,9 @@
]
},
"nsis": {
- "oneClick": true,
+ "oneClick": false,
"perMachine": false,
- "allowToChangeInstallationDirectory": false,
+ "allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "SOCDOF Desktop",
@@ -43,6 +45,9 @@
"installerIcon": "public/socdof_icon.ico",
"uninstallerIcon": "public/socdof_icon.ico",
"installerHeaderIcon": "public/socdof_icon.ico",
- "uninstallerSidebar": "public/uninstallerSidebar.bmp"
+ "uninstallerSidebar": "public/uninstallerSidebar.bmp",
+ "installerSidebar": "public/installerSidebar.bmp",
+ "installerHeader": "public/installerHeader.bmp",
+ "displayLanguageSelector": false
}
}
diff --git a/electron/main.cjs b/electron/main.cjs
index 1fc49ac..e5cee66 100644
--- a/electron/main.cjs
+++ b/electron/main.cjs
@@ -15,6 +15,42 @@ if (!gotSingleInstanceLock) {
}
let mainWindow;
+let splashWindow = null;
+
+// Show a lightweight branded window while the main workspace initializes.
+function createSplashWindow() {
+ if (splashWindow && !splashWindow.isDestroyed()) return;
+ splashWindow = new BrowserWindow({
+ width: 390,
+ height: 300,
+ frame: false,
+ transparent: false,
+ resizable: false,
+ movable: true,
+ center: true,
+ show: false,
+ skipTaskbar: false,
+ backgroundColor: '#101116',
+ title: 'SOCDOF wird gestartet',
+ icon: process.platform === 'win32'
+ ? path.join(__dirname, '../public/socdof_icon.ico')
+ : path.join(__dirname, '../public/socdof_icon.png'),
+ webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true }
+ });
+ splashWindow.once('ready-to-show', () => {
+ if (splashWindow && !splashWindow.isDestroyed()) splashWindow.show();
+ });
+ splashWindow.on('closed', () => { splashWindow = null; });
+ splashWindow.loadFile(path.join(__dirname, 'splash.html'));
+}
+
+function closeSplashWindow() {
+ if (splashWindow && !splashWindow.isDestroyed()) {
+ const current = splashWindow;
+ splashWindow = null;
+ current.close();
+ }
+}
let mobileSyncHttpServer = null;
const mobileSyncStore = new Map();
@@ -461,6 +497,7 @@ function createWindow() {
// Reveal window once first paint is completed and DOM is ready
mainWindow.once('ready-to-show', () => {
if (mainWindow) {
+ closeSplashWindow();
mainWindow.setFullScreen(true);
mainWindow.show();
mainWindow.focus();
@@ -1565,6 +1602,7 @@ app.on('second-instance', () => {
});
app.whenReady().then(() => {
+ createSplashWindow();
createWindow();
// Defer background language seed & backup folder readiness so the UI thread renders immediately
diff --git a/electron/splash.html b/electron/splash.html
new file mode 100644
index 0000000..50794cd
--- /dev/null
+++ b/electron/splash.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+SOCDOF
+
+
+
+
+
+SOCDOF
+Der Arbeitsbereich wird vorbereitet …
+
+
+
+
+
diff --git a/installer-bootstrapper/Program.Framework.cs b/installer-bootstrapper/Program.Framework.cs
new file mode 100644
index 0000000..c1dcdbe
--- /dev/null
+++ b/installer-bootstrapper/Program.Framework.cs
@@ -0,0 +1,297 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Web.Script.Serialization;
+using System.Windows.Forms;
+
+namespace SOCDOF.Bootstrapper
+{
+ internal static class FrameworkProgram
+ {
+ [STAThread]
+ private static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new SetupWindow());
+ }
+ }
+
+ internal sealed class SetupWindow : Form
+ {
+ private readonly Color bg = Color.FromArgb(30, 31, 35);
+ private readonly Color panel = Color.FromArgb(40, 42, 47);
+ private readonly Color muted = Color.FromArgb(165, 168, 180);
+ private readonly Color accent = Color.FromArgb(111, 126, 255);
+ private readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
+ private readonly Label status = new Label();
+ private readonly Label detail = new Label();
+ private readonly Label version = new Label();
+ private readonly Label pathText = new Label();
+ private readonly ProgressBar bar = new ProgressBar();
+ private readonly Button install = new Button();
+ private readonly Button browse = new Button();
+ private string target = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "SOCDOF");
+ private string packagePath;
+ private bool downloaded;
+ private bool installing;
+ private bool installed;
+
+ public SetupWindow()
+ {
+ Text = "SOCDOF Installer";
+ StartPosition = FormStartPosition.CenterScreen;
+ FormBorderStyle = FormBorderStyle.None;
+ ClientSize = new Size(620, 430);
+ MinimumSize = new Size(620, 430);
+ MaximumSize = new Size(620, 430);
+ BackColor = bg;
+ ForeColor = Color.White;
+ ShowInTaskbar = true;
+ Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
+ Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1, 18, 18));
+ Compose();
+ Shown += async delegate { await DownloadLatestAsync(); };
+ FormClosed += delegate { http.Dispose(); };
+ }
+
+ private void Compose()
+ {
+ Panel top = new Panel { Dock = DockStyle.Top, Height = 48, BackColor = bg };
+ top.MouseDown += Drag;
+ Controls.Add(top);
+ PictureBox smallLogo = new PictureBox { Location = new Point(22, 13), Size = new Size(22, 22), SizeMode = PictureBoxSizeMode.Zoom, Image = Icon == null ? null : Icon.ToBitmap(), BackColor = Color.Transparent };
+ top.Controls.Add(smallLogo);
+ Label title = new Label { Text = "SOCDOF Setup", Location = new Point(52, 14), AutoSize = true, ForeColor = Color.FromArgb(226, 228, 235), Font = new Font("Segoe UI", 9.5f) };
+ title.MouseDown += Drag;
+ top.Controls.Add(title);
+ Button close = new Button { Text = "×", Location = new Point(568, 6), Size = new Size(44, 36), FlatStyle = FlatStyle.Flat, BackColor = bg, ForeColor = Color.FromArgb(205, 207, 215), Font = new Font("Segoe UI", 16f), Cursor = Cursors.Hand, Anchor = AnchorStyles.Top | AnchorStyles.Right };
+ close.FlatAppearance.BorderSize = 0;
+ close.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 55, 65);
+ close.Click += delegate { Close(); };
+ top.Controls.Add(close);
+
+ Controls.Add(new PictureBox { Location = new Point(42, 78), Size = new Size(58, 58), SizeMode = PictureBoxSizeMode.Zoom, Image = Icon == null ? null : Icon.ToBitmap(), BackColor = Color.Transparent });
+ Controls.Add(new Label { Text = "SOCDOF installieren", Location = new Point(118, 78), Size = new Size(440, 36), Font = new Font("Segoe UI Semibold", 20f, FontStyle.Bold), ForeColor = Color.White });
+ version.SetBounds(120, 117, 440, 22);
+ version.Font = new Font("Segoe UI", 9.5f);
+ version.ForeColor = muted;
+ version.Text = "Neueste Version wird gesucht …";
+ Controls.Add(version);
+ Controls.Add(new Panel { Location = new Point(36, 157), Size = new Size(548, 1), BackColor = Color.FromArgb(57, 59, 66) });
+
+ status.SetBounds(40, 181, 530, 27);
+ status.Font = new Font("Segoe UI Semibold", 11f, FontStyle.Bold);
+ status.ForeColor = Color.FromArgb(240, 241, 246);
+ status.Text = "Installation wird vorbereitet …";
+ Controls.Add(status);
+ detail.SetBounds(40, 211, 540, 36);
+ detail.Font = new Font("Segoe UI", 9f);
+ detail.ForeColor = muted;
+ detail.Text = "SOCDOF wird sicher heruntergeladen und vorbereitet.";
+ Controls.Add(detail);
+ bar.SetBounds(40, 257, 540, 5);
+ bar.Style = ProgressBarStyle.Marquee;
+ bar.MarqueeAnimationSpeed = 25;
+ bar.BackColor = panel;
+ bar.ForeColor = accent;
+ Controls.Add(bar);
+
+ Controls.Add(new Label { Text = "INSTALLATIONSORDNER", Location = new Point(40, 286), AutoSize = true, Font = new Font("Segoe UI Semibold", 8f, FontStyle.Bold), ForeColor = muted });
+ Panel pathPanel = new Panel { Location = new Point(40, 307), Size = new Size(540, 42), BackColor = panel, Padding = new Padding(12, 0, 0, 0) };
+ Controls.Add(pathPanel);
+ pathText.SetBounds(12, 0, 405, 42);
+ pathText.Text = target;
+ pathText.TextAlign = ContentAlignment.MiddleLeft;
+ pathText.Font = new Font("Segoe UI", 9f);
+ pathText.ForeColor = Color.FromArgb(224, 226, 234);
+ pathText.AutoEllipsis = true;
+ pathPanel.Controls.Add(pathText);
+ browse.Text = "Ändern";
+ browse.SetBounds(431, 6, 96, 30);
+ browse.FlatStyle = FlatStyle.Flat;
+ browse.FlatAppearance.BorderColor = Color.FromArgb(75, 78, 88);
+ browse.BackColor = panel;
+ browse.ForeColor = Color.White;
+ browse.Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold);
+ browse.Cursor = Cursors.Hand;
+ browse.Click += Browse;
+ pathPanel.Controls.Add(browse);
+
+ install.Text = "Download wird vorbereitet …";
+ install.SetBounds(40, 366, 540, 42);
+ install.FlatStyle = FlatStyle.Flat;
+ install.FlatAppearance.BorderSize = 0;
+ install.BackColor = accent;
+ install.ForeColor = Color.White;
+ install.Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold);
+ install.Cursor = Cursors.Hand;
+ install.Enabled = false;
+ install.Click += async delegate { if (installed) Launch(); else await InstallAsync(); };
+ Controls.Add(install);
+ Controls.Add(new Label { Text = "STRUDELCODE · YURI", Location = new Point(40, 411), Size = new Size(540, 14), TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 7f, FontStyle.Bold), ForeColor = Color.FromArgb(103, 106, 117) });
+ }
+
+ private async Task DownloadLatestAsync()
+ {
+ State("Neueste Version wird gesucht …", "Verbindung zu GitHub wird hergestellt.", 0, true);
+ install.Text = "Download wird vorbereitet …";
+ install.Enabled = false;
+ try
+ {
+ http.DefaultRequestHeaders.UserAgent.Clear();
+ http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SOCDOF-Installer", "1.0"));
+ http.DefaultRequestHeaders.Accept.Clear();
+ http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
+ string json = await http.GetStringAsync("https://api.github.com/repos/Strudelcode/SOCDOF/releases/latest");
+ Dictionary release = new JavaScriptSerializer().Deserialize>(json);
+ version.Text = "Version " + Convert.ToString(release["tag_name"]) + " · Windows 64-Bit";
+ string url = null;
+ long size = 0;
+ ArrayList assets = release["assets"] as ArrayList;
+ if (assets != null)
+ {
+ foreach (object item in assets)
+ {
+ Dictionary asset = item as Dictionary;
+ if (asset == null) continue;
+ string name = Convert.ToString(asset["name"]);
+ if (name.StartsWith("SOCDOF-Setup-", StringComparison.OrdinalIgnoreCase) && name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ {
+ url = Convert.ToString(asset["browser_download_url"]);
+ if (asset.ContainsKey("size")) long.TryParse(Convert.ToString(asset["size"]), out size);
+ break;
+ }
+ }
+ }
+ if (String.IsNullOrEmpty(url)) throw new InvalidOperationException("Kein passendes SOCDOF-Setup im neuesten Release gefunden.");
+ packagePath = Path.Combine(Path.GetTempPath(), "SOCDOF-Setup-" + Guid.NewGuid().ToString("N") + ".exe");
+ State("SOCDOF wird heruntergeladen …", "Das Installationspaket wird im Hintergrund geladen.", 0, false);
+ using (HttpResponseMessage response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
+ {
+ response.EnsureSuccessStatusCode();
+ long total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : size;
+ using (Stream input = await response.Content.ReadAsStreamAsync())
+ using (FileStream output = new FileStream(packagePath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true))
+ {
+ byte[] buffer = new byte[81920];
+ long got = 0;
+ int read;
+ while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ await output.WriteAsync(buffer, 0, read);
+ got += read;
+ if (total > 0)
+ {
+ int pct = (int)Math.Max(0, Math.Min(100, got * 100L / total));
+ bar.Value = pct;
+ detail.Text = "Installationspaket wird heruntergeladen … " + pct + "%";
+ }
+ }
+ await output.FlushAsync();
+ }
+ }
+ downloaded = true;
+ State("Bereit zur Installation", "Sie können den Installationsordner noch ändern.", 100, false);
+ install.Text = "Installation starten";
+ install.Enabled = true;
+ }
+ catch (Exception ex)
+ {
+ State("Download nicht möglich", "Prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.", 0, false);
+ detail.Text = ex.Message.Length > 110 ? ex.Message.Substring(0, 110) + "…" : ex.Message;
+ install.Text = "Erneut versuchen";
+ install.Enabled = true;
+ }
+ }
+
+ private void Browse(object sender, EventArgs e)
+ {
+ using (FolderBrowserDialog dialog = new FolderBrowserDialog())
+ {
+ dialog.Description = "Wählen Sie den übergeordneten Ordner für SOCDOF.";
+ dialog.ShowNewFolderButton = true;
+ dialog.SelectedPath = Path.GetDirectoryName(target);
+ if (dialog.ShowDialog(this) == DialogResult.OK)
+ {
+ target = Path.Combine(dialog.SelectedPath, "SOCDOF");
+ pathText.Text = target;
+ }
+ }
+ }
+
+ private async Task InstallAsync()
+ {
+ if (!downloaded) { await DownloadLatestAsync(); return; }
+ if (installing || String.IsNullOrEmpty(packagePath) || !File.Exists(packagePath)) return;
+ installing = true;
+ install.Enabled = false;
+ browse.Enabled = false;
+ State("SOCDOF wird installiert …", "Die Anwendung wird eingerichtet. Bitte lassen Sie dieses Fenster geöffnet.", 0, true);
+ try
+ {
+ Directory.CreateDirectory(target);
+ ProcessStartInfo info = new ProcessStartInfo { FileName = packagePath, Arguments = "/S /D=" + target, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = Path.GetTempPath() };
+ using (Process process = Process.Start(info))
+ {
+ if (process == null) throw new InvalidOperationException("Das Setup konnte nicht gestartet werden.");
+ await Task.Run(delegate { process.WaitForExit(); });
+ if (process.ExitCode != 0) throw new InvalidOperationException("Das Setup wurde mit Fehlercode " + process.ExitCode + " beendet.");
+ }
+ installed = true;
+ State("Installation abgeschlossen", "SOCDOF wurde erfolgreich installiert.", 100, false);
+ install.Text = "SOCDOF starten";
+ install.Enabled = true;
+ }
+ catch (Exception ex)
+ {
+ State("Installation fehlgeschlagen", "Das Setup konnte nicht abgeschlossen werden.", 0, false);
+ detail.Text = ex.Message.Length > 110 ? ex.Message.Substring(0, 110) + "…" : ex.Message;
+ install.Text = "Erneut versuchen";
+ install.Enabled = true;
+ }
+ finally { installing = false; browse.Enabled = true; }
+ }
+
+ private void Launch()
+ {
+ string exe = Path.Combine(target, "SOCDOF.exe");
+ if (!File.Exists(exe))
+ {
+ MessageBox.Show(this, "Die Anwendung wurde nicht gefunden:\n" + exe, "SOCDOF", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+ try { Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); Close(); }
+ catch (Exception ex) { MessageBox.Show(this, ex.Message, "SOCDOF konnte nicht gestartet werden", MessageBoxButtons.OK, MessageBoxIcon.Error); }
+ }
+
+ private void State(string heading, string description, int percent, bool marquee)
+ {
+ status.Text = heading;
+ detail.Text = description;
+ if (marquee) { bar.Style = ProgressBarStyle.Marquee; bar.MarqueeAnimationSpeed = 25; }
+ else { bar.Style = ProgressBarStyle.Continuous; bar.MarqueeAnimationSpeed = 0; bar.Value = Math.Max(0, Math.Min(100, percent)); }
+ }
+
+ private static void Drag(object sender, MouseEventArgs e)
+ {
+ if (e.Button != MouseButtons.Left) return;
+ Control c = sender as Control;
+ Form f = c == null ? null : c.FindForm();
+ if (f == null) return;
+ ReleaseCapture();
+ SendMessage(f.Handle, 0xA1, 0x2, 0);
+ }
+
+ [DllImport("user32.dll")] private static extern bool ReleaseCapture();
+ [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
+ [DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int left, int top, int right, int bottom, int width, int height);
+ }
+}
diff --git a/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj b/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj
new file mode 100644
index 0000000..3e0abdf
--- /dev/null
+++ b/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Release
+ AnyCPU
+ {A8B33B8E-8F45-43A1-9F56-63A0B8D9140E}
+ WinExe
+ SOCDOF.Bootstrapper
+ SOCDOF-Installer
+ v4.8
+ ..\public\socdof_icon.ico
+ ..\release\bootstrapper\
+ x64
+ true
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 3d7e4f6..2182b4b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "socdof",
"private": true,
- "version": "24.9.1",
+ "version": "24.10.0",
"description": "Strudel's Organization, Commerce & Documentation Offline Flow - Offline-first Windows ERP Desktop Suite & Web Workspace",
"author": "Yuri / Strudel",
"publisher": "Yuri / Strudel",
diff --git a/src/components/CustomDownloadModal.tsx b/src/components/CustomDownloadModal.tsx
index af160e8..198ba60 100644
--- a/src/components/CustomDownloadModal.tsx
+++ b/src/components/CustomDownloadModal.tsx
@@ -57,7 +57,7 @@ export const CustomDownloadModal: React.FC = ({
const [downloadProgress, setDownloadProgress] = useState(0);
const [latestVersion, setLatestVersion] = useState(APP_VERSION);
const [downloadUrl, setDownloadUrl] = useState(
- `https://github.com/Strudelcode/SOCDOF/releases/download/v${APP_VERSION}/SOCDOF-Setup-${APP_VERSION}.exe`
+ `https://github.com/Strudelcode/SOCDOF/releases/download/v${APP_VERSION}/SOCDOF-Installer.exe`
);
const [isLocallyDownloaded, setIsLocallyDownloaded] = useState(() => isDesktopInstalledOrDownloaded());
@@ -74,7 +74,9 @@ export const CustomDownloadModal: React.FC = ({
if (!isMounted || !data) return;
const tag = data.tag_name ? data.tag_name.replace(/^v/, '') : APP_VERSION;
setLatestVersion(tag || APP_VERSION);
- const exeAsset = data.assets?.find((a: any) =>
+ const exeAsset = data.assets?.find((a: any) =>
+ typeof a.name === 'string' && a.name.toLowerCase() === 'socdof-installer.exe'
+ ) ?? data.assets?.find((a: any) =>
typeof a.name === 'string' && a.name.toLowerCase().endsWith('.exe')
);
if (exeAsset?.browser_download_url) {
@@ -308,7 +310,7 @@ export const CustomDownloadModal: React.FC = ({
Offizielles SOCDOF Windows Setup
- Dateiname: SOCDOF-Setup-{latestVersion}.exe (~95 MB)
+ Dateiname: SOCDOF-Installer.exe (öffnet sofort ein Installationsfenster)
@@ -345,7 +347,7 @@ export const CustomDownloadModal: React.FC = ({
{downloadStep === 'downloading' ? (
<>
- Download läuft: SOCDOF-Setup-{latestVersion}.exe
+ Download läuft: SOCDOF-Installer.exe
>
) : (
<>
@@ -398,7 +400,7 @@ export const CustomDownloadModal: React.FC = ({
Datei herunterladen
- Die Datei SOCDOF-Setup-{latestVersion}.exe wird in Ihrem gewohnten Windows-Download-Ordner gespeichert.
+ Die Datei SOCDOF-Installer.exe wird in Ihrem gewohnten Windows-Download-Ordner gespeichert.
diff --git a/src/components/WindowsExeNotificationToast.tsx b/src/components/WindowsExeNotificationToast.tsx
index 322dc4b..9223c7b 100644
--- a/src/components/WindowsExeNotificationToast.tsx
+++ b/src/components/WindowsExeNotificationToast.tsx
@@ -50,7 +50,12 @@ export const WindowsExeNotificationToast: React.FC typeof a.name === 'string' && a.name.toLowerCase().endsWith('.exe'));
+ const assets = Array.isArray(data.assets) ? data.assets : [];
+ const exeAsset = assets.find((asset: any) =>
+ typeof asset.name === 'string' && asset.name.toLowerCase() === 'socdof-installer.exe'
+ ) ?? assets.find((asset: any) =>
+ typeof asset.name === 'string' && asset.name.toLowerCase().endsWith('.exe')
+ );
if (exeAsset?.browser_download_url) {
setDownloadUrl(exeAsset.browser_download_url);
} else if (data.html_url) {
diff --git a/src/lib/updateChecker.ts b/src/lib/updateChecker.ts
index 19eb03b..18a5b96 100644
--- a/src/lib/updateChecker.ts
+++ b/src/lib/updateChecker.ts
@@ -153,10 +153,12 @@ export async function checkForAppUpdates(): Promise {
const cleanTag = tagName.replace(/^v/i, '').trim();
const hasUpdate = compareSemver(cleanTag, APP_VERSION) > 0;
- // Find any .exe attachment asset if present
- const exeAsset = Array.isArray(data.assets)
- ? data.assets.find((a: any) => a.name?.toLowerCase().endsWith('.exe'))
- : null;
+ // The in-app updater must download the actual NSIS setup, not the
+ // instant-launch bootstrapper (which is interactive and does not accept updater flags).
+ const assets = Array.isArray(data.assets) ? data.assets : [];
+ const exeAsset = assets.find((asset: any) =>
+ typeof asset.name === 'string' && /^socdof-setup-.*\.exe$/i.test(asset.name)
+ ) || null;
return {
hasUpdate,
diff --git a/src/lib/version.ts b/src/lib/version.ts
index cc5dfc1..e44e609 100644
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -5,7 +5,7 @@ export interface VersionRelease {
highlights: string[];
}
-export const APP_VERSION = '24.9.1';
+export const APP_VERSION = '24.10.0';
export const APP_NAME = 'SOCDOF';
export const APP_FULL_NAME = "Strudel's Organization, Commerce & Documentation Offline Flow";
export const APP_AUTHOR = 'Yuri / Strudel';
@@ -13,6 +13,17 @@ export const APP_LOCATION = 'South Tyrol, Italy';
export const APP_COPYRIGHT = '© Strudel';
export const VERSION_HISTORY: VersionRelease[] = [
+ {
+ version: '24.10.0',
+ date: '2026-10-13',
+ title: 'Selectable Installer & Immediate Branded Startup Splash',
+ highlights: [
+ 'Selectable Windows installation directory while retaining per-user installation under Local AppData by default',
+ 'Added a compact branded startup splash that appears while the SOCDOF workspace initializes, then closes when the main window is ready',
+ 'Applied SOCDOF installer header and sidebar branding to the location-selection setup flow',
+ 'Added a lightweight Windows bootstrapper that opens immediately, downloads the latest setup with real progress, and supports a custom installation directory'
+ ]
+ },
{
version: '24.9.1',
date: '2026-10-13',
diff --git a/todo/todo.md b/todo/todo.md
index 0e2be47..219c937 100644
--- a/todo/todo.md
+++ b/todo/todo.md
@@ -21,6 +21,8 @@
## 2. Active Roadmap & Pending Tasks
+- [ ] Verify v24.10.0 instant-launch bootstrapper and startup splash on Windows: test custom install path, Local AppData default, upgrade over an existing install, and startup behavior; the NSIS installer still has a native setup phase before a custom app window can launch.
+
### 2.5 System Optimization & Continuous Polishing
- [ ] Continuous module performance and responsive UX refinements.
- [ ] Automated regression coverage for critical local data workflows.
diff --git a/versions/V24.md b/versions/V24.md
index 9985ce2..5067d8c 100644
--- a/versions/V24.md
+++ b/versions/V24.md
@@ -4,6 +4,20 @@ All notable changes and technical implementation details for SOCDOF Version 24.x
---
+## Version 24.10.0 — Selectable Installer & Immediate Branded Startup Splash
+- **Instant-Launch Windows Bootstrapper (`installer-bootstrapper/`)**:
+ - Added a lightweight .NET Framework 4.8 setup window that appears before the full Electron installer is downloaded.
+ - Displays real download progress, supports choosing an install directory, runs the packaged NSIS installer silently, and offers to launch SOCDOF when complete.
+ - Uses the latest official GitHub Release as the source for the signed/packaged setup executable.
+- **Selectable Windows Installation Directory (`electron-builder.json`)**:
+ - Re-enabled the guided setup flow with a user-selectable installation directory while retaining per-user installation and the Local AppData-based default.
+ - Applied SOCDOF's custom installer header and sidebar artwork to the setup flow.
+- **Branded Startup Feedback (`electron/main.cjs` & `electron/splash.html`)**:
+ - Added a lightweight frameless splash window that appears while the Electron workspace initializes and closes once the main window is ready.
+ - Kept the splash lightweight so it does not wait for the full React workspace before showing status feedback.
+
+---
+
## Version 24.9.1 — Smart Homescreen Download Intelligence & Version-Aware Update Prompts
- **Homescreen & Start Menu Download Visibility Intelligence (`src/lib/desktopDownloadStatus.ts` & `src/components/DesktopWindowWorkspace.tsx`)**:
- Implemented version-aware evaluation for the Start Menu and Homescreen download banners: If the user has already downloaded or installed the desktop app, the download banner is completely hidden.
diff --git a/versions/releases/v24-release.md b/versions/releases/v24-release.md
index 1cd1d7f..dfaf5dd 100644
--- a/versions/releases/v24-release.md
+++ b/versions/releases/v24-release.md
@@ -1,5 +1,13 @@
# SOCDOF v24 Release Overview
+## v24.10.0 — Selectable Installer & Immediate Branded Startup Splash
+
+- Added an instant-launch Windows bootstrapper with real download progress, install-directory selection, and a clean dark setup window.
+
+- Restored a guided Windows setup flow with a selectable installation directory and a Local AppData per-user default.
+- Applied SOCDOF's custom installer header and sidebar assets to the setup flow.
+- Added a minimal frameless branded splash while the desktop workspace initializes, dismissing it when the main window is ready.
+
## v24.6.2 — Polished Settings Overview & Clear Section Navigation
- Replaced the low-contrast native compact-window selector with a grouped themed menu that supports Escape and outside-click dismissal.