Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ffa9c17
fix: port js-project atlas correctness fixes to Python (B1/B2/B3, A1/…
com55 Jun 24, 2026
803bb88
feat: add repack_multi_page for multi-page atlas repack (A3 phase A)
com55 Jun 24, 2026
ddd5a11
feat: wire multi-page repack into the modify-mode controller (A3 phas…
com55 Jun 24, 2026
92d065f
feat: multi-page atlas page switcher UI + preview API (A3 phase C)
com55 Jun 24, 2026
b8357cc
fix: hide repack toggle in multi-page modify mode
com55 Jun 24, 2026
f97a0b2
feat: onedir standalone + Inno Setup installer, installer-based self-…
com55 Jun 24, 2026
e75c7d8
fix: pin installer dir (DisableDirPage) + document installer migration
com55 Jun 24, 2026
641415e
feat(installer): optional .atlas file association
com55 Jun 25, 2026
632e6cb
feat(installer): mutexes, version info, clean upgrade, quick launch +…
com55 Jun 25, 2026
ac5daec
fix(installer): use x64compatible architecture identifier
com55 Jun 25, 2026
9b5306a
feat(installer): clean upgrades via previous-version uninstall (drop …
com55 Jun 25, 2026
5a6f6bf
fix(modify): full-canvas merge, sequential mods, and repack toggle
com55 Jun 28, 2026
c2a9e59
feat(ui): redesign layout with app bar, view/edit modes, and save image
com55 Jun 28, 2026
b9fcb81
refactor: reorganize Python code into atlas_toolkit package
com55 Jun 28, 2026
379f525
refactor(ui): split monolithic script and stylesheet into modules
com55 Jun 28, 2026
5606f26
feat(app): missing-page modal, mod guards, preview cache, and --debug
com55 Jun 28, 2026
d004d5c
fix(app): add debounce to dragover event handler
com55 Jun 28, 2026
a7f0263
chore: bump version to 0.3.0
com55 Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 84 additions & 29 deletions .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,19 @@ jobs:
$version | Out-File -FilePath VERSION -Encoding utf8 -NoNewline
echo "Building version: $version"

- name: Build with Nuitka 🏗️
- name: Build with Nuitka (standalone / onedir) 🏗️
uses: Nuitka/Nuitka-Action@main
with:
nuitka-version: main
script-name: main.py
mode: onefile
mode: standalone
windows-console-mode: ${{ env.CONSOLE_MODE }}
windows-icon-from-ico: ui/icon.ico
include-data-dir: ui=ui
include-data-files: |
VERSION=VERSION
self_update_helper.py=self_update_helper.py
include-package-data: webview
include-package: atlas_toolkit
nofollow-import-to: |
PyQt5
PyQt6
Expand All @@ -93,8 +93,22 @@ jobs:
tkinter
gi
output-dir: dist
output-file: AtlasToolkit.exe
onefile-tempdir-spec: "{TEMP}/AtlasToolkit"

- name: Normalize standalone exe name 🔧
shell: pwsh
run: |
$dist = "dist/main.dist"
# standalone mode names the exe after the script (main.exe); output-file
# is ignored for standalone, so rename to the user-facing name.
if (Test-Path "$dist/main.exe") {
Rename-Item -Path "$dist/main.exe" -NewName "AtlasToolkit.exe" -Force
}
if (-not (Test-Path "$dist/AtlasToolkit.exe")) {
Write-Host "Expected exe not found. Contents of ${dist}:"
Get-ChildItem -Recurse $dist | Select-Object FullName
throw "AtlasToolkit.exe missing after standalone build"
}
Write-Host "OK: $dist/AtlasToolkit.exe present"

- name: Check signing secrets
id: check_secrets
Expand All @@ -106,18 +120,18 @@ jobs:
echo "available=false" >> $GITHUB_OUTPUT
fi

- name: Sign Executable
- name: Sign Standalone Executable
if: steps.check_secrets.outputs.available == 'true'
shell: pwsh
env:
CERT_DATA: ${{ secrets.CERT_BASE64 }}
CERT_PASS: ${{ secrets.CERT_PASSWORD }}
run: |
$certPath = Join-Path $env:RUNNER_TEMP "cert.pfx"
$exePath = "dist/AtlasToolkit.exe"
$exePath = "dist/main.dist/AtlasToolkit.exe"

$signtool = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin\" -Filter signtool.exe -Recurse |
Where-Object { $_.FullName -match "x64" } |
$signtool = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin\" -Filter signtool.exe -Recurse |
Where-Object { $_.FullName -match "x64" } |
Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName

try {
Expand All @@ -129,9 +143,9 @@ jobs:
certutil -p $env:CERT_PASS -dump $certPath | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Incorrect password or corrupted PFX file." }

Write-Host "Signing executable..."
Write-Host "Signing standalone executable..."
& $signtool sign /f $certPath /p "$env:CERT_PASS" /tr http://timestamp.digicert.com /td sha256 /fd sha256 /v $exePath

if ($LASTEXITCODE -ne 0) { throw "SignTool failed with code: $LASTEXITCODE" }
}
catch {
Expand All @@ -142,35 +156,76 @@ jobs:
if (Test-Path $certPath) { Remove-Item $certPath -Force }
}

- name: Prepare Assets for Upload 📦
shell: powershell
id: prepare_assets
- name: Install Inno Setup 🧰
shell: pwsh
run: choco install innosetup --no-progress -y

- name: Build Installer (Inno Setup) 📦
shell: pwsh
run: |
$exePath = "dist/AtlasToolkit.exe"
# Release (from workflow_call or tag push)
if ("${{ env.IS_RELEASE }}" -eq 'true') {
Compress-Archive -Path $exePath -DestinationPath "AtlasToolkit-Windows-x64.zip"
echo "ARTIFACT_PATH=AtlasToolkit-Windows-x64.zip" >> $env:GITHUB_OUTPUT
$version = "${{ steps.build_version.outputs.version }}"
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) { throw "ISCC.exe not found at $iscc" }
& $iscc "/DMyAppVersion=$version" AtlasToolkit.iss
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with code: $LASTEXITCODE" }
if (-not (Test-Path "installer/AtlasToolkit-Setup-x64.exe")) {
throw "Installer not produced at installer/AtlasToolkit-Setup-x64.exe"
}

- name: Sign Installer
if: steps.check_secrets.outputs.available == 'true'
shell: pwsh
env:
CERT_DATA: ${{ secrets.CERT_BASE64 }}
CERT_PASS: ${{ secrets.CERT_PASSWORD }}
run: |
$certPath = Join-Path $env:RUNNER_TEMP "cert.pfx"
$exePath = "installer/AtlasToolkit-Setup-x64.exe"

$signtool = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin\" -Filter signtool.exe -Recurse |
Where-Object { $_.FullName -match "x64" } |
Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName

try {
$cleanBase64 = $env:CERT_DATA -replace '\s',''
$certBytes = [System.Convert]::FromBase64String($cleanBase64)
[System.IO.File]::WriteAllBytes($certPath, $certBytes)

Write-Host "Signing installer..."
& $signtool sign /f $certPath /p "$env:CERT_PASS" /tr http://timestamp.digicert.com /td sha256 /fd sha256 /v $exePath
if ($LASTEXITCODE -ne 0) { throw "SignTool failed with code: $LASTEXITCODE" }
}
# Manual (workflow_dispatch)
else {
echo "ARTIFACT_NAME=AtlasToolkit-Windows-x64" >> $env:GITHUB_OUTPUT
echo "ARTIFACT_PATH=$exePath" >> $env:GITHUB_OUTPUT
catch {
Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
finally {
if (Test-Path $certPath) { Remove-Item $certPath -Force }
}

- name: Create portable zip 📁
shell: pwsh
run: |
Compress-Archive -Path "dist/main.dist/*" -DestinationPath "AtlasToolkit-Windows-x64-portable.zip" -Force
if (-not (Test-Path "AtlasToolkit-Windows-x64-portable.zip")) { throw "Portable zip not produced" }

- name: Upload Artifact (Testing) 💾
- name: Upload Artifacts (Testing) 💾
uses: actions/upload-artifact@v4
if: ${{ steps.prepare_assets.outputs.ARTIFACT_NAME != '' }}
if: ${{ env.IS_RELEASE != 'true' }}
with:
name: ${{ steps.prepare_assets.outputs.ARTIFACT_NAME }}
path: ${{ steps.prepare_assets.outputs.ARTIFACT_PATH }}
name: AtlasToolkit-Windows-x64
path: |
installer/AtlasToolkit-Setup-x64.exe
AtlasToolkit-Windows-x64-portable.zip

- name: Upload Release Asset 📤
- name: Upload Release Assets 📤
if: ${{ env.IS_RELEASE == 'true' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
target_commitish: ${{ github.sha }}
files: AtlasToolkit-Windows-x64.zip
files: |
installer/AtlasToolkit-Setup-x64.exe
AtlasToolkit-Windows-x64-portable.zip
generate_release_notes: true
token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ wheels/
.venv
.vscode
.claude
test*
CONTEXT.md
129 changes: 129 additions & 0 deletions AtlasToolkit.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
; AtlasToolkit — Inno Setup script (per-user install, no admin required).
; Version is injected by CI: ISCC.exe /DMyAppVersion=1.2.3 AtlasToolkit.iss
; Packages the Nuitka standalone output in dist\main.dist into a single Setup.exe.

#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif

#define MyAppName "AtlasToolkit"
#define MyAppExeName "AtlasToolkit.exe"
#define MyAppProgId "AtlasToolkit.atlas"
#define MyAppMutex "AtlasToolkitSingleInstanceMutex"
#define MyAppPublisher "com55"
#define MyAppURL "https://github.com/com55/AtlasToolkit"

[Setup]
; AppId MUST stay constant across releases so upgrades replace in place (never change it).
AppId={{AADE8604-EC9B-491C-92FA-D0628C934556}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}/releases
; Per-user install — no admin / no UAC, so silent self-update works unattended.
PrivilegesRequired=lowest
DefaultDirName={localappdata}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
; Force the fixed install location that _is_installed_build() / silent self-update
; assume — without this the user could install elsewhere and never get auto-updates.
DisableDirPage=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
OutputDir=installer
OutputBaseFilename=AtlasToolkit-Setup-x64
SetupIconFile=ui\icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
VersionInfoVersion={#MyAppVersion}
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
; Detect a running instance (app holds this named mutex) and block a second
; installer from running concurrently.
AppMutex={#MyAppMutex}
SetupMutex=AtlasToolkitSetupMutex
; During a silent self-update, close the running app via Restart Manager; the
; updater cmd handles relaunch, so don't let Inno restart it (avoids double-launch).
CloseApplications=yes
RestartApplications=no
; Notify the shell when the .atlas association changes (refreshes icons / Open With).
ChangesAssociations=yes

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "associate"; Description: "Associate .atlas files with {#MyAppName}"; GroupDescription: "File associations:"

[Files]
Source: "dist\main.dist\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion

[Registry]
; Per-user .atlas file association (HKA -> HKCU under PrivilegesRequired=lowest).
; Only written when the "associate" task is selected; cleaned up on uninstall.
Root: HKA; Subkey: "Software\Classes\.atlas\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppProgId}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associate
Root: HKA; Subkey: "Software\Classes\.atlas"; ValueType: string; ValueName: ""; ValueData: "{#MyAppProgId}"; Flags: uninsdeletevalue; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppProgId}"; ValueType: string; ValueName: ""; ValueData: "Spine Atlas File"; Flags: uninsdeletekey; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppProgId}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Tasks: associate
Root: HKA; Subkey: "Software\Classes\{#MyAppProgId}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Tasks: associate

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon

[Run]
; Interactive install only — the silent self-update relaunch is owned by the updater cmd.
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent

[UninstallRun]
; On uninstall, drop the update cache (downloaded installers / logs / scripts);
; user config (config.json) in the same data dir is intentionally left intact.
Filename: "{cmd}"; Parameters: "/c rmdir /s /q ""{localappdata}\{#MyAppName}\update"""; Flags: runhidden; RunOnceId: "DelUpdateCache"

[Code]
{ Clean upgrade without blunt deletion: run the PREVIOUS version's uninstaller
before installing the new files. It removes only what the old version installed
(per its uninstall log) — so stale files from a dropped dependency are cleaned —
while leaving app-created data (config.json, update cache) untouched. }

function GetUninstallString(): String;
var
Key: String;
S: String;
begin
Key := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{AADE8604-EC9B-491C-92FA-D0628C934556}_is1';
S := '';
if not RegQueryStringValue(HKCU, Key, 'UninstallString', S) then
RegQueryStringValue(HKLM, Key, 'UninstallString', S);
Result := S;
end;

procedure CurStepChanged(CurStep: TSetupStep);
var
UnInstStr, OldExe: String;
ResultCode, Waited: Integer;
begin
if CurStep <> ssInstall then
Exit;
UnInstStr := GetUninstallString();
if UnInstStr = '' then
Exit;
UnInstStr := RemoveQuotes(UnInstStr);
OldExe := ExtractFilePath(UnInstStr) + '{#MyAppExeName}';
if Exec(UnInstStr, '/VERYSILENT /NORESTART /SUPPRESSMSGBOXES', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
{ The uninstaller relaunches a temp copy and returns early; wait until the old
exe is actually gone so it can't delete our freshly-copied files (cap ~20s). }
Waited := 0;
while FileExists(OldExe) and (Waited < 100) do
begin
Sleep(200);
Waited := Waited + 1;
end;
end;
end;
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ View atlas regions, extract individual sprites, replace sprites with other image

- Now, Web version is available [here](https://com55.github.io/AtlasToolkit/)!
- You can use the pre-built executables or download stable version source code from [Releases](https://github.com/com55/AtlasToolkit/releases/latest).
- Windows is distributed as an **installer** (`AtlasToolkit-Setup-x64.exe`, per-user, no admin) plus a **portable** zip. The installed build updates itself silently in-app; the portable build links to the releases page.
- **One-time migration:** builds made before the installer switch used the old single-exe auto-update and will report "asset not found" when checking for this release — download and run the new installer once manually.
- Or, clone the repository and run from source (in-development / unstable):
```bash
git clone https://github.com/com55/AtlasToolkit.git
Expand Down
Loading