Merge branch 'fix/windows-startup-and-installer' into custom

This commit is contained in:
Murilo Scolari 2026-10-25 02:29:03 -03:00
commit 25c2d8b784
8 changed files with 245 additions and 101 deletions

View file

@ -169,9 +169,6 @@ jobs:
cp scripts/windows/install.bat "$stage/"
cp output/simpmusic.crt "$stage/"
cp "$msix" "$stage/"
if [ -f output/simpmusic.appinstaller ]; then
cp output/simpmusic.appinstaller "$stage/"
fi
( cd "$stage" && zip -q -j \
"${GITHUB_WORKSPACE}/installers/SimpMusic-Windows-installer.zip" \
* )
@ -194,13 +191,10 @@ jobs:
cp "$zsync" staging_desktop/SimpMusic-v${VERSION}-Linux-x86_64.AppImage.zsync
fi
# Windows offline installer zip and MSIX / appinstaller
# Windows offline installer zip and standalone MSIX
if [ -f installers/SimpMusic-Windows-installer.zip ]; then
cp installers/SimpMusic-Windows-installer.zip staging_desktop/SimpMusic-v${VERSION}-Windows-x64-installer.zip
fi
if [ -f output/simpmusic.appinstaller ]; then
cp output/simpmusic.appinstaller staging_desktop/simpmusic.appinstaller
fi
msix=$(ls output/simpmusic-*.x64.msix 2>/dev/null | head -1)
if [ -n "$msix" ]; then
cp "$msix" staging_desktop/

View file

@ -420,3 +420,6 @@
# Preserve BuildKonfig and VersionManager for runtime version/hash checks
-keep class com.maxrave.simpmusic.BuildKonfig { *; }
-keep class com.maxrave.simpmusic.utils.VersionManager { *; }
# Preserve Windows Taskbar Thumbnail Controller and JNA Win32 callbacks
-keep class com.maxrave.simpmusic.WindowsTaskbarThumbnailController** { *; }

View file

@ -618,7 +618,19 @@ fun runDesktopApp(args: Array<String> = emptyArray()) {
window.toFront()
window.requestFocus()
} else {
window.toFront()
javax.swing.SwingUtilities.invokeLater {
isAwtIconified = false
isOccluded = false
if (!window.isVisible) {
window.isVisible = true
}
if ((window.extendedState and Frame.ICONIFIED) != 0) {
window.extendedState = window.extendedState and Frame.ICONIFIED.inv()
}
window.toFront()
window.requestFocus()
DesktopGpuManager.onRestored(window)
}
}
}
}

View file

@ -10,6 +10,7 @@ import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Structure
import com.sun.jna.WString
import com.sun.jna.platform.win32.BaseTSD
import com.sun.jna.platform.win32.Guid
import com.sun.jna.platform.win32.Ole32
import com.sun.jna.platform.win32.User32
@ -25,6 +26,7 @@ import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.Window
import java.awt.image.BufferedImage
import javax.swing.SwingUtilities
/**
* Native Windows Taskbar Thumbnail Toolbar Controller (ITaskbarList3).
@ -55,6 +57,15 @@ object WindowsTaskbarThumbnailController {
private const val ID_PLAY_PAUSE = 102
private const val ID_NEXT = 103
private val SUBCLASS_ID = BaseTSD.ULONG_PTR(9001L)
// Strong reference to prevent Garbage Collection of JNA native callbacks
@Volatile
private var activeSubclassProc: SubclassProcCallback? = null
@Volatile
private var activeWndProc: WindowProcCallback? = null
@Structure.FieldOrder("dwMask", "iId", "iBitmap", "hIcon", "szTip", "dwFlags")
open class THUMBBUTTON : Structure {
@JvmField var dwMask: Int = 0
@ -98,6 +109,34 @@ object WindowsTaskbarThumbnailController {
}
}
private interface CustomComctl32 : Library {
fun SetWindowSubclass(
hWnd: WinDef.HWND,
pfnSubclass: SubclassProcCallback,
uIdSubclass: BaseTSD.ULONG_PTR,
dwRefData: BaseTSD.DWORD_PTR,
): Boolean
fun RemoveWindowSubclass(
hWnd: WinDef.HWND,
pfnSubclass: SubclassProcCallback,
uIdSubclass: BaseTSD.ULONG_PTR,
): Boolean
fun DefSubclassProc(
hWnd: WinDef.HWND,
uMsg: Int,
wParam: WinDef.WPARAM,
lParam: WinDef.LPARAM,
): WinDef.LRESULT
companion object {
val INSTANCE: CustomComctl32 by lazy {
Native.load("comctl32", CustomComctl32::class.java, W32APIOptions.UNICODE_OPTIONS)
}
}
}
private interface CustomGdi32 : Library {
fun CreateBitmap(nWidth: Int, nHeight: Int, nPlanes: Int, nBitCount: Int, lpBits: IntArray): WinDef.HBITMAP?
fun CreateBitmap(nWidth: Int, nHeight: Int, nPlanes: Int, nBitCount: Int, lpBits: ByteArray): WinDef.HBITMAP?
@ -110,6 +149,17 @@ object WindowsTaskbarThumbnailController {
}
}
private interface SubclassProcCallback : Callback {
fun callback(
hWnd: WinDef.HWND,
uMsg: Int,
wParam: WinDef.WPARAM,
lParam: WinDef.LPARAM,
uIdSubclass: BaseTSD.ULONG_PTR,
dwRefData: BaseTSD.DWORD_PTR,
): WinDef.LRESULT
}
private interface WindowProcCallback : Callback {
fun callback(
hwnd: WinDef.HWND,
@ -212,15 +262,24 @@ object WindowsTaskbarThumbnailController {
buttons[2].setTooltip("Next")
buttons[2].dwFlags = THBF_ENABLED
fun addButtons() {
var buttonsAdded = false
fun addButtonsSafe() {
if (buttonsAdded) return
buttons[0].write()
buttons[1].write()
buttons[2].write()
val res = taskbar.thumbBarAddButtons(hwnd, 3, buttons[0].pointer)
Logger.d(TAG, "thumbBarAddButtons result: $res")
if (res == 0) {
buttonsAdded = true
}
}
fun updateButtons(isPlaying: Boolean, hasPrev: Boolean, hasNext: Boolean) {
fun updateButtonsSafe(isPlaying: Boolean, hasPrev: Boolean, hasNext: Boolean) {
if (!buttonsAdded) {
addButtonsSafe()
}
buttons[0].dwFlags = if (hasPrev) THBF_ENABLED else THBF_DISABLED
buttons[1].hIcon = if (isPlaying) pauseIcon else playIcon
buttons[1].setTooltip(if (isPlaying) "Pause" else "Play")
@ -233,58 +292,114 @@ object WindowsTaskbarThumbnailController {
taskbar.thumbBarUpdateButtons(hwnd, 3, buttons[0].pointer)
}
addButtons()
val taskbarCreatedMsg = CustomUser32.INSTANCE.RegisterWindowMessageW(WString("TaskbarButtonCreated"))
var oldWndProc: Pointer? = null
val wndProc = object : WindowProcCallback {
fun handleCommandMessage(wParam: WinDef.WPARAM): Boolean {
val hiWord = (wParam.toLong() shr 16) and 0xFFFF
if (hiWord == THBN_CLICKED.toLong()) {
val id = (wParam.toLong() and 0xFFFF).toInt()
when (id) {
ID_PREV -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.Previous) }
ID_PLAY_PAUSE -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.PlayPause) }
ID_NEXT -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.Next) }
}
return true
}
return false
}
// Attempt safe window subclassing via comctl32 first
val subclassProc = object : SubclassProcCallback {
override fun callback(
hwnd: WinDef.HWND,
hWnd: WinDef.HWND,
uMsg: Int,
wParam: WinDef.WPARAM,
lParam: WinDef.LPARAM,
uIdSubclass: BaseTSD.ULONG_PTR,
dwRefData: BaseTSD.DWORD_PTR,
): WinDef.LRESULT {
if (uMsg == taskbarCreatedMsg) {
addButtons()
} else if (uMsg == WM_COMMAND) {
val hiWord = (wParam.toLong() shr 16) and 0xFFFF
if (hiWord == THBN_CLICKED.toLong()) {
val id = (wParam.toLong() and 0xFFFF).toInt()
when (id) {
ID_PREV -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.Previous) }
ID_PLAY_PAUSE -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.PlayPause) }
ID_NEXT -> coroutineScope.launch { mediaPlayerHandler.onPlayerEvent(PlayerEvent.Next) }
}
return WinDef.LRESULT(0)
}
SwingUtilities.invokeLater { addButtonsSafe() }
return CustomComctl32.INSTANCE.DefSubclassProc(hWnd, uMsg, wParam, lParam)
}
val prev = oldWndProc
return if (prev != null) {
CustomUser32.INSTANCE.CallWindowProc(prev, hwnd, uMsg, wParam, lParam)
} else {
User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam)
if (uMsg == WM_COMMAND && handleCommandMessage(wParam)) {
return WinDef.LRESULT(0)
}
return CustomComctl32.INSTANCE.DefSubclassProc(hWnd, uMsg, wParam, lParam)
}
}
oldWndProc = CustomUser32.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, wndProc)
// Keep strong reference to prevent GC
activeSubclassProc = subclassProc
val usedSubclass = runCatching {
CustomComctl32.INSTANCE.SetWindowSubclass(hwnd, subclassProc, SUBCLASS_ID, BaseTSD.DWORD_PTR(0L))
}.getOrDefault(false)
var oldWndProc: Pointer? = null
var fallbackWndProc: WindowProcCallback? = null
if (!usedSubclass) {
Logger.w(TAG, "comctl32 SetWindowSubclass failed, falling back to SetWindowLongPtr")
fallbackWndProc = object : WindowProcCallback {
override fun callback(
hwnd: WinDef.HWND,
uMsg: Int,
wParam: WinDef.WPARAM,
lParam: WinDef.LPARAM,
): WinDef.LRESULT {
if (uMsg == taskbarCreatedMsg) {
SwingUtilities.invokeLater { addButtonsSafe() }
} else if (uMsg == WM_COMMAND && handleCommandMessage(wParam)) {
return WinDef.LRESULT(0)
}
val prev = oldWndProc
return if (prev != null) {
CustomUser32.INSTANCE.CallWindowProc(prev, hwnd, uMsg, wParam, lParam)
} else {
User32.INSTANCE.DefWindowProc(hwnd, uMsg, wParam, lParam)
}
}
}
activeWndProc = fallbackWndProc
oldWndProc = CustomUser32.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, fallbackWndProc)
}
// If the window is already showing when attach() is invoked, initialize buttons on EDT
SwingUtilities.invokeLater {
if (window.isShowing) {
addButtonsSafe()
}
}
val stateJob = coroutineScope.launch {
mediaPlayerHandler.controlState.collectLatest { state ->
updateButtons(
isPlaying = state.isPlaying,
hasPrev = state.isPreviousAvailable,
hasNext = state.isNextAvailable,
)
SwingUtilities.invokeLater {
updateButtonsSafe(
isPlaying = state.isPlaying,
hasPrev = state.isPreviousAvailable,
hasNext = state.isNextAvailable,
)
}
}
}
val cleanup: () -> Unit = {
stateJob.cancel()
val prev = oldWndProc
if (prev != null) {
CustomUser32.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, prev)
if (usedSubclass) {
runCatching {
CustomComctl32.INSTANCE.RemoveWindowSubclass(hwnd, subclassProc, SUBCLASS_ID)
}
if (activeSubclassProc === subclassProc) {
activeSubclassProc = null
}
} else {
val prev = oldWndProc
if (prev != null) {
CustomUser32.INSTANCE.SetWindowLongPtr(hwnd, GWLP_WNDPROC, prev)
}
if (activeWndProc === fallbackWndProc) {
activeWndProc = null
}
}
prevIcon?.let { CustomUser32.INSTANCE.DestroyIcon(it) }
playIcon?.let { CustomUser32.INSTANCE.DestroyIcon(it) }

View file

@ -278,7 +278,7 @@ actual object PlatformAppUpdater {
val msix = downloadedMsixPath
try {
if (msix != null && File(msix).exists()) {
val psScript = "Start-Sleep -Milliseconds 800; Add-AppxPackage -Path '$msix' -ForceApplicationShutdown -ForceUpdateFromAnyVersion; start 'shell:appsFolder\\Simpmusic_ejp2bhxmz1qq6!Simpmusic'"
val psScript = "Start-Sleep -Milliseconds 800; try { Add-AppxPackage -Path '$msix' -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop } catch { Get-AppxPackage -Name '*Simpmusic*' -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction SilentlyContinue; Add-AppxPackage -Path '$msix' -ForceApplicationShutdown -ErrorAction SilentlyContinue }; start 'shell:appsFolder\\Simpmusic_ejp2bhxmz1qq6!Simpmusic'"
ProcessBuilder("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript).start()
} else {
val psScript = "Start-Sleep -Milliseconds 800; start 'shell:appsFolder\\Simpmusic_ejp2bhxmz1qq6!Simpmusic'"

View file

@ -56,9 +56,10 @@ app {
// Release for this repo (Conveyor 22.0+ behaviour, per the March 2024
// blog update).
//
// updates = background → Conveyor wires the background .appinstaller
// update check for Windows so that Windows checks for updates silently.
updates = background
// updates = none → Conveyor does not wire Sparkle (Mac) or the
// background .appinstaller update check (Windows). Releases are
// handled by SimpMusic's native in-app updater (AppUpdater.jvm.kt).
updates = none
site {
consistency-checks = warn
@ -165,14 +166,6 @@ app {
windows {
start-menu.group = "SimpMusic"
manifests.msix.app-installer {
hours-between-update-checks = 4
show-prompt = false
update-blocks-activation = false
automatic-background-task = true
check-on-launch = true
}
// DEBUG ONLY — remove before shipping a release.
// A Windows GUI binary has no console attached, so everything the app writes to
// stdout/stderr is discarded and a startup failure leaves nothing to look at.

View file

@ -1,7 +1,7 @@
[versions]
# App version
version-name = "2.3.4"
version-code = "64"
version-name = "2.3.5"
version-code = "65"
android = "9.4.1"
kotlin = "2.4.20"

View file

@ -1,86 +1,113 @@
@echo off
REM SimpMusic offline installer for Windows.
REM SimpMusic offline installer for Windows (x64 / ARM64 via Prism emulation).
REM Double-click this file. UAC will prompt for admin (needed for cert + sideload).
REM
REM Bundle requirement (same folder as this .bat):
REM - simpmusic.crt
REM - simpmusic-<version>.msix (any .msix matching simpmusic-*.msix)
REM
REM Only x64 MSIX is shipped — Windows 11 ARM64 hosts run it under Prism
REM emulation. ARM64 native build is blocked upstream by a missing
REM androidx.sqlite-bundled windows-arm64 native binary.
REM - simpmusic-<version>.x64.msix
setlocal EnableExtensions
cd /d "%~dp0"
set "SIMPMUSIC_DIR=%~dp0"
REM --- 0. Pre-flight check (ensure files were extracted from ZIP) --------------
if not exist "%SIMPMUSIC_DIR%simpmusic.crt" (
echo.
echo [ERRO / ERROR]
echo Os arquivos nao foram encontrados nesta pasta!
echo Certifique-se de EXTRAIR o conteudo do arquivo ZIP antes de executar o instalador.
echo (Nao execute o arquivo diretamente de dentro do ZIP compactado).
echo.
echo Files not found next to install.bat.
echo Please EXTRACT all files from the ZIP before running install.bat.
echo.
pause
exit /b 1
)
set "SIMPMUSIC_MSIX="
for %%f in ("%SIMPMUSIC_DIR%simpmusic-*.msix") do set "SIMPMUSIC_MSIX=%%f"
if not defined SIMPMUSIC_MSIX (
echo.
echo [ERRO / ERROR]
echo Nenhum pacote simpmusic-*.msix foi encontrado nesta pasta.
echo Extraia todos os arquivos do instalador ZIP para uma pasta comum e tente novamente.
echo.
echo No simpmusic-*.msix package found in this folder.
echo.
pause
exit /b 1
)
REM --- 1. Self-elevate to admin -----------------------------------------------
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting admin elevation...
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
echo Solicitando elevacao de administrador / Requesting admin elevation...
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%comspec%' -ArgumentList '/c `\"`\"%~f0`\"`\"' -Verb RunAs"
exit /b
)
echo.
echo === SimpMusic offline installer ===
echo ===========================================
echo SimpMusic Offline Installer
echo ===========================================
echo.
REM --- 2. Install cert to Trusted People (idempotent via certutil) -----------
if not exist "%~dp0simpmusic.crt" (
echo [ERROR] simpmusic.crt not found next to install.bat
pause
exit /b 1
)
echo [1/3] Importing simpmusic.crt to LocalMachine\TrustedPeople ...
certutil -addstore -f "TrustedPeople" "%~dp0simpmusic.crt"
echo [1/3] Importando certificado simpmusic.crt para TrustedPeople...
certutil -addstore -f "TrustedPeople" "%SIMPMUSIC_DIR%simpmusic.crt"
if %errorlevel% neq 0 (
echo [ERROR] Certificate import failed.
pause
exit /b 1
)
REM --- 3. Find the bundled MSIX ----------------------------------------------
set "MSIX="
for %%f in ("%~dp0simpmusic-*.msix") do set "MSIX=%%f"
if not defined MSIX (
echo [ERROR] No simpmusic-*.msix found in this folder.
echo.
echo [ERRO] Falha ao importar o certificado de assinatura / Certificate import failed.
pause
exit /b 1
)
REM --- 3. Close lingering processes and install MSIX -------------------------
echo.
echo [2/3] Installing/updating SimpMusic...
set "APPINSTALLER=%~dp0simpmusic.appinstaller"
if exist "%APPINSTALLER%" (
echo Registering with online update support ^(AppInstaller^)...
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"try { Add-AppxPackage -AppInstallerFile '%APPINSTALLER%' -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop; exit 0 } catch { Write-Host 'AppInstaller registration failed, falling back to direct MSIX...'; try { Add-AppxPackage -Path '%MSIX%' -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop; exit 0 } catch { Write-Host $_; exit 1 } }"
) else (
echo Installing MSIX package: "%MSIX%" ...
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"try { Add-AppxPackage -Path '%MSIX%' -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop; exit 0 } catch { Write-Host 'In-place update failed; removing existing SimpMusic and reinstalling...'; Get-AppxPackage -Name 'Simpmusic' | Remove-AppxPackage -ErrorAction SilentlyContinue; try { Add-AppxPackage -Path '%MSIX%' -ForceApplicationShutdown -ErrorAction Stop; exit 0 } catch { Write-Host $_; exit 1 } }"
)
echo [2/3] Instalando/atualizando o SimpMusic...
set "SIMPMUSIC_CRT=%SIMPMUSIC_DIR%simpmusic.crt"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$msix = $env:SIMPMUSIC_MSIX; " ^
"Get-Process -Name 'SimpMusic','updatecheck' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue; " ^
"try { " ^
" Add-AppxPackage -Path $msix -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop; " ^
" Write-Host 'Pacote MSIX instalado/atualizado com sucesso.'; " ^
" exit 0 " ^
"} catch { " ^
" Write-Host 'Atualizacao direta falhou. Removendo versao anterior e reinstalando...'; " ^
" Get-AppxPackage -Name '*Simpmusic*' -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction SilentlyContinue; " ^
" try { " ^
" Add-AppxPackage -Path $msix -ForceApplicationShutdown -ErrorAction Stop; " ^
" Write-Host 'Instalacao limpa concluida com sucesso.'; " ^
" exit 0 " ^
" } catch { " ^
" Write-Host $_; " ^
" exit 1 " ^
" } " ^
"}"
if %errorlevel% neq 0 (
echo.
echo [ERROR] Add-AppxPackage failed. Common fixes:
echo - Enable 'Install apps from any source, including loose files'
echo ^(Settings -^> Privacy ^& security -^> For developers^)
echo - Restart Windows and run install.bat again
echo [ERRO] Falha ao instalar o aplicativo via Add-AppxPackage.
echo Sugestoes para resolver:
echo 1. Ative o 'Modo de Desenvolvedor' no Windows:
echo Configuracoes -^> Privacidade e Seguranca -^> Para desenvolvedores -^> Modo de Desenvolvedor
echo 2. Verifique se o Windows Update esta ativo
echo 3. Reinicie o computador e execute install.bat novamente
echo.
pause
exit /b 1
)
REM --- 4. Launch app ---------------------------------------------------------
echo.
echo [3/3] Launching SimpMusic ...
echo [3/3] Iniciando o SimpMusic / Launching SimpMusic ...
start "" "shell:appsFolder\Simpmusic_ejp2bhxmz1qq6!Simpmusic"
echo.
echo SimpMusic installed successfully.
echo To uninstall: Settings -^> Apps -^> Installed apps -^> SimpMusic -^> Uninstall
echo SimpMusic instalado com sucesso! / SimpMusic installed successfully.
echo.
pause
endlocal