Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 29 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,24 @@ jobs:
python .github/scripts/update_version_info.py "$VERSION"

# Update .iss files (Inno Setup installer scripts)
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$VERSION\"/" switchcraft_legacy.iss
# Extract numeric version only (remove .dev0, +build, etc.) for VersionInfoVersion
BASE_VERSION=$(echo "$VERSION" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
# VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build)
VERSION_INFO="${BASE_VERSION}.0"
# For MyAppVersion: use full version for dev (with commit ID), but remove commit ID for beta/stable
if [[ "${{ github.event.inputs.release_type }}" == "development" ]]; then
# Dev release: keep full version with commit ID (e.g., "2026.1.2.dev0+9d07a00")
APP_VERSION="$VERSION"
else
# Beta/Stable: remove commit ID if present (e.g., "2026.1.2b1" or "2026.1.2")
APP_VERSION=$(echo "$VERSION" | sed -E 's/\+[a-zA-Z0-9.-]+$//')
fi
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$APP_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$APP_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_legacy.iss

- name: Generate Changelog
id: changelog
Expand Down Expand Up @@ -171,12 +185,19 @@ jobs:
sed -i "s/__version__ = \".*\"/__version__ = \"$DEV_VERSION\"/" src/switchcraft/__init__.py
python .github/scripts/update_version_info.py "$DEV_VERSION"

# Update .iss files (Inno Setup installer scripts) - keep base version without .dev0 suffix for installer
BASE_VERSION=$(echo $DEV_VERSION | sed 's/\.dev0.*$//')
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$BASE_VERSION\"/" switchcraft_modern.iss
# Update .iss files (Inno Setup installer scripts)
# Extract numeric version only (remove .dev0, +build, etc.) for VersionInfoVersion
BASE_VERSION=$(echo "$DEV_VERSION" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
# VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build)
VERSION_INFO="${BASE_VERSION}.0"
# For dev releases: MyAppVersion should include commit ID (full DEV_VERSION)
# MyAppVersionNumeric should be numeric only (BASE_VERSION)
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$DEV_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$BASE_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$DEV_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_legacy.iss
Comment on lines +188 to +200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same indentation concern applies here.

The sed patterns in this step have the same potential issue with indented #define lines as noted above. Apply the same fix with \(^[[:space:]]*\) capture group to preserve indentation.

🤖 Prompt for AI Agents
In @.github/workflows/release.yml around lines 188 - 200, The sed replacements
that set MyAppVersion, MyAppVersionNumeric, and MyAppVersionInfo in both
switchcraft_modern.iss and switchcraft_legacy.iss should preserve any leading
indentation; update each sed invocation that currently matches "#define
MyAppVersion", "#define MyAppVersionNumeric" and "#define MyAppVersionInfo" to
capture and reuse leading whitespace (e.g., a capture for ^[[:space:]]*) so the
replacement reinserts the same indentation while substituting DEV_VERSION,
BASE_VERSION and VERSION_INFO respectively for the six occurrences in the diff.


# Update fallback versions in build scripts and version generator
# Update build_release.ps1 fallback
Expand Down
34 changes: 27 additions & 7 deletions scripts/build_release.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,23 @@ Write-Host "Project Root: $RepoRoot" -ForegroundColor Gray
$PyProjectFile = Join-Path $RepoRoot "pyproject.toml"
# Fallback version if extraction fails (can be overridden via env variable)
$AppVersion = if ($env:SWITCHCRAFT_VERSION) { $env:SWITCHCRAFT_VERSION } else { "2026.1.2" }
$AppVersionNumeric = $AppVersion -replace '-.*', '' # Remove suffixes like -dev for numeric parsing
# Extract numeric version only (remove .dev0, +build, -dev, etc.) for VersionInfoVersion
# Pattern: extract MAJOR.MINOR.PATCH from any version format
$AppVersionNumeric = if ($AppVersion -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { $AppVersion -replace '[^0-9.].*', '' }
# VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build)
$AppVersionInfo = "$AppVersionNumeric.0"

if (Test-Path $PyProjectFile) {
try {
$VersionLine = Get-Content -Path $PyProjectFile | Select-String "version = " | Select-Object -First 1
if ($VersionLine -match 'version = "(.*)"') {
$AppVersion = $Matches[1]
$AppVersionNumeric = $AppVersion -replace '-.*', '' # Remove suffixes like -dev for numeric parsing
Write-Host "Detected Version: $AppVersion (Numeric base: $AppVersionNumeric)" -ForegroundColor Cyan
# Extract numeric version only (remove .dev0, +build, -dev, etc.) for VersionInfoVersion
# Pattern: extract MAJOR.MINOR.PATCH from any version format
$AppVersionNumeric = if ($AppVersion -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { $AppVersion -replace '[^0-9.].*', '' }
# VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build)
$AppVersionInfo = "$AppVersionNumeric.0"
Write-Host "Detected Version: $AppVersion (Numeric base: $AppVersionNumeric, Info: $AppVersionInfo)" -ForegroundColor Cyan
} else {
Write-Warning "Could not parse version from pyproject.toml, using fallback: $AppVersion"
}
Expand Down Expand Up @@ -364,12 +372,18 @@ if ($Installer -and $IsWinBuild) {

if (Test-Path "switchcraft_modern.iss") {
Write-Host "Compiling Modern Installer..."
& $IsccPath "/DMyAppVersion=$AppVersion" "/DMyAppVersionNumeric=$AppVersionNumeric" "switchcraft_modern.iss" | Out-Null
& $IsccPath "/DMyAppVersion=$AppVersion" "/DMyAppVersionNumeric=$AppVersionNumeric" "/DMyAppVersionInfo=$AppVersionInfo" "switchcraft_modern.iss" | Out-Null

# Cleanup temporary SwitchCraft.exe used for bundling
if (Test-Path $ModernExe) { Remove-Item $ModernExe -Force }

Write-Host "Installer Created: SwitchCraft-Setup.exe" -ForegroundColor Green
# Get full path to created installer (OutputDir is "dist" in switchcraft_modern.iss)
$InstallerPath = Join-Path (Resolve-Path "dist") "SwitchCraft-Setup.exe"
if (Test-Path $InstallerPath) {
Write-Host "Installer Created: $InstallerPath" -ForegroundColor Green
} else {
Write-Host "Installer Created: SwitchCraft-Setup.exe (in dist)" -ForegroundColor Green
}
}
}
else {
Expand Down Expand Up @@ -409,8 +423,14 @@ if ($Legacy -and $IsWinBuild) {
Write-Host "`nBuilding Legacy Installer..." -ForegroundColor Cyan
$IsccPath = Get-InnoSetupPath
if ($IsccPath -and (Test-Path "switchcraft_legacy.iss")) {
& $IsccPath "/DMyAppVersion=$AppVersion" "/DMyAppVersionNumeric=$AppVersionNumeric" "switchcraft_legacy.iss" | Out-Null
Write-Host "Installer Created: SwitchCraft-Legacy-Setup.exe" -ForegroundColor Green
& $IsccPath "/DMyAppVersion=$AppVersion" "/DMyAppVersionNumeric=$AppVersionNumeric" "/DMyAppVersionInfo=$AppVersionInfo" "switchcraft_legacy.iss" | Out-Null
# Get full path to created installer (OutputDir is "dist" in switchcraft_legacy.iss)
$LegacyInstallerPath = Join-Path (Resolve-Path "dist") "SwitchCraft-Legacy-Setup.exe"
if (Test-Path $LegacyInstallerPath) {
Write-Host "Installer Created: $LegacyInstallerPath" -ForegroundColor Green
} else {
Write-Host "Installer Created: SwitchCraft-Legacy-Setup.exe (in dist)" -ForegroundColor Green
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/switchcraft/assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@
"lbl_active_cert": "Aktives Zertifikat:",
"cert_not_configured": "Nicht konfiguriert",
"cert_not_found": "Keine Code-Signing-Zertifikate gefunden.",
"cert_gpo_detected": "GPO-konfiguriertes Zertifikat erkannt.",
"cert_auto_detected": "Zertifikat automatisch erkannt",
"cert_auto_detected_multi": "Mehrere Zertifikate gefunden, verwende erstes",
"cert_detect_failed": "Zertifikatserkennung fehlgeschlagen",
Expand Down Expand Up @@ -482,6 +483,8 @@
"copied_to_clipboard": "In Zwischenablage kopiert!",
"not_assigned": "Nicht zugewiesen.",
"no_description": "Keine Beschreibung.",
"no_groups_found": "Keine Gruppen gefunden.",
"group_deselected": "Gruppe abgewählt",
"all_users_devices": "Alle Benutzer/Geräte",
"logs_no_logs_found": "Keine Log-Dateien zum Exportieren gefunden.",
"logs_export_failed": "Log-Export fehlgeschlagen.",
Expand Down
3 changes: 3 additions & 0 deletions src/switchcraft/assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@
"lbl_active_cert": "Active Certificate:",
"cert_not_configured": "Not Configured",
"cert_not_found": "No code signing certificates found.",
"cert_gpo_detected": "GPO-configured certificate detected.",
"cert_auto_detected": "Certificate auto-detected",
"cert_auto_detected_multi": "Multiple certs found, using first",
"cert_detect_failed": "Certificate detection failed",
Expand Down Expand Up @@ -459,6 +460,8 @@
"copied_to_clipboard": "Copied to clipboard!",
"not_assigned": "Not assigned.",
"no_description": "No description.",
"no_groups_found": "No groups found.",
"group_deselected": "Group deselected",
"all_users_devices": "All Users/Devices",
"logs_no_logs_found": "No log files found to export.",
"logs_export_failed": "Log export failed.",
Expand Down
31 changes: 27 additions & 4 deletions src/switchcraft/gui_modern/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,14 @@ def update_ui():
logging.getLogger(__name__).warning(f"Failed to update wizard UI: {ex}")

if self.page:
self.page.run_task(update_ui)
# Wrap sync function in async wrapper for run_task
import inspect
if inspect.iscoroutinefunction(update_ui):
self.page.run_task(update_ui)
else:
async def async_update_ui():
update_ui()
self.page.run_task(async_update_ui)

threading.Thread(target=_base_install, daemon=True).start()

Expand Down Expand Up @@ -1745,7 +1752,13 @@ def skip_demo(e):
self.page.open(dlg)

# Show dialog on UI thread
self.page.run_task(show_demo_dialog)
import inspect
if inspect.iscoroutinefunction(show_demo_dialog):
self.page.run_task(show_demo_dialog)
else:
async def async_show_demo_dialog():
show_demo_dialog()
self.page.run_task(async_show_demo_dialog)

def _start_demo_analysis(self):
"""
Expand Down Expand Up @@ -1797,7 +1810,10 @@ def download_and_analyze():
if 'analyzer' in self._view_cache:
analyzer_view = self._view_cache['analyzer']
if hasattr(analyzer_view, 'start_analysis'):
self.page.run_task(lambda: analyzer_view.start_analysis(tmp.name))
# Wrap lambda in async wrapper for run_task
async def async_start_analysis():
analyzer_view.start_analysis(tmp.name)
self.page.run_task(async_start_analysis)

except Exception as e:
logger.error(f"Demo failed: {e}")
Expand All @@ -1818,7 +1834,14 @@ def open_download(e):
)
self.page.open(dlg)

self.page.run_task(show_error)
# Wrap sync function in async wrapper for run_task
import inspect
if inspect.iscoroutinefunction(show_error):
self.page.run_task(show_error)
else:
async def async_show_error():
show_error()
self.page.run_task(async_show_error)

threading.Thread(target=download_and_analyze, daemon=True).start()

Expand Down
44 changes: 44 additions & 0 deletions src/switchcraft/gui_modern/utils/view_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@ def _open_path(self, path):
except Exception as e2:
self._show_snack(f"Failed to open path: {path}", "RED")
logger.error(f"Failed to open path: {e2}")
def _run_task_safe(self, func):
"""
Safely run a function via page.run_task, wrapping sync functions in async wrappers.

This helper ensures that both sync and async functions can be passed to run_task
without causing TypeError: handler must be a coroutine function.

Parameters:
func: A callable (sync or async function)

Returns:
bool: True if task was scheduled, False otherwise
"""
import inspect

try:
page = getattr(self, "app_page", None)
if not page:
try:
page = self.page
except (RuntimeError, AttributeError):
return False
if not page or not hasattr(page, 'run_task'):
return False

# Check if function is already async
if inspect.iscoroutinefunction(func):
page.run_task(func)
return True
else:
# Wrap sync function in async wrapper
async def async_wrapper():
func()
page.run_task(async_wrapper)
return True
except Exception as ex:
logger.debug(f"Failed to run task safely: {ex}")
# Fallback: try direct call
try:
func()
return True
except Exception:
return False

def _close_dialog(self, dialog=None):
"""Close a dialog on the page."""
try:
Expand Down
81 changes: 53 additions & 28 deletions src/switchcraft/gui_modern/views/dashboard_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ def __init__(self, page: ft.Page):
height=280
)
], spacing=20, wrap=True)
], spacing=15),
padding=20 # Consistent padding with other views
], spacing=15, expand=True),
padding=20, # Consistent padding with other views
expand=True
)
]

Expand Down Expand Up @@ -156,17 +157,18 @@ def _refresh_ui(self):
], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=4)
)

chart_content = ft.Column([
ft.Text(i18n.get("chart_activity_title") or "Activity (Last 5 Days)", weight=ft.FontWeight.BOLD, size=18),
ft.Container(height=20),
ft.Row(bars, alignment=ft.MainAxisAlignment.SPACE_EVENLY, vertical_alignment=ft.CrossAxisAlignment.END),
])

# Ensure chart container has content
if not self.chart_container.content:
self.chart_container.content = chart_content
# Get the existing chart container's content Column
chart_col = self.chart_container.content
if isinstance(chart_col, ft.Column) and len(chart_col.controls) >= 3:
# Update the bars row (index 2)
chart_col.controls[2] = ft.Row(bars, alignment=ft.MainAxisAlignment.SPACE_EVENLY, vertical_alignment=ft.CrossAxisAlignment.END)
else:
# Update existing content
# Rebuild entire chart content if structure is wrong
chart_content = ft.Column([
ft.Text(i18n.get("chart_activity_title") or "Activity (Last 5 Days)", weight=ft.FontWeight.BOLD, size=18),
ft.Container(height=20),
ft.Row(bars, alignment=ft.MainAxisAlignment.SPACE_EVENLY, vertical_alignment=ft.CrossAxisAlignment.END),
])
self.chart_container.content = chart_content

# Recent
Expand Down Expand Up @@ -201,26 +203,49 @@ def _refresh_ui(self):
)
)

recent_content = ft.Column([
ft.Text(i18n.get("recent_actions") or "Recent Actions", weight=ft.FontWeight.BOLD, size=18),
ft.Container(height=10),
ft.Column(recent_list, spacing=8, scroll=ft.ScrollMode.AUTO, expand=True)
], expand=True)

# Ensure recent container has content
if not self.recent_container.content:
self.recent_container.content = recent_content
# Get the existing recent container's content Column
recent_col = self.recent_container.content
if isinstance(recent_col, ft.Column) and len(recent_col.controls) >= 3:
# Update the list column (index 2)
recent_col.controls[2] = ft.Column(recent_list, spacing=8, scroll=ft.ScrollMode.AUTO, expand=True)
else:
# Update existing content
# Rebuild entire recent content if structure is wrong
recent_content = ft.Column([
ft.Text(i18n.get("recent_actions") or "Recent Actions", weight=ft.FontWeight.BOLD, size=18),
ft.Container(height=10),
ft.Column(recent_list, spacing=8, scroll=ft.ScrollMode.AUTO, expand=True)
], expand=True)
self.recent_container.content = recent_content

# Force update of all containers
try:
self.chart_container.update()
self.recent_container.update()
self.update()
except Exception:
pass
# Use run_task to ensure updates happen on UI thread
if hasattr(self, 'app_page') and hasattr(self.app_page, 'run_task'):
def update_ui():
try:
self.chart_container.update()
self.recent_container.update()
self.update()
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Failed to update dashboard UI: {e}")

# Wrap in async if needed
import inspect
if inspect.iscoroutinefunction(update_ui):
self.app_page.run_task(update_ui)
else:
async def async_update():
update_ui()
self.app_page.run_task(async_update)
else:
# Fallback: direct update
try:
self.chart_container.update()
self.recent_container.update()
self.update()
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Failed to update dashboard UI: {e}")


def _stat_card(self, label, value, icon, color):
Expand Down
Loading
Loading