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.