From 72fd4a92f4624659437add1bdfa4386231ac8531 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 17:06:33 -0500 Subject: [PATCH] updating Windows install experience --- scripts/build_package_linux.sh | 37 +++++++++++++- scripts/linux_postinst.sh | 26 ++++++++++ .../android-file-handler-linux.spec | 1 - .../android-file-handler-windows.spec | 2 +- scripts/windows/first_run_install.ps1 | 51 +++++++++++++++++++ src/gui/license_agreement.py | 43 +++++++++++++++- src/main.py | 2 + 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 scripts/linux_postinst.sh create mode 100644 scripts/windows/first_run_install.ps1 diff --git a/scripts/build_package_linux.sh b/scripts/build_package_linux.sh index b4443dc..40a5c67 100755 --- a/scripts/build_package_linux.sh +++ b/scripts/build_package_linux.sh @@ -17,6 +17,39 @@ chmod +x dist/android-file-handler # this ensures the package installs /usr/local/bin/android-file-handler (no extra dist/ prefix) # Debian +# Prepare packaging layout in a separate directory to avoid clobbering build artifacts +PKG_DIR="pkg_dist" +rm -rf "$PKG_DIR" +mkdir -p "$PKG_DIR/usr/local/bin" \ + "$PKG_DIR/usr/share/applications" \ + "$PKG_DIR/usr/share/icons/hicolor/256x256/apps" + +# Copy built binary +cp dist/android-file-handler "$PKG_DIR/usr/local/bin/android-file-handler" +chmod 0755 "$PKG_DIR/usr/local/bin/android-file-handler" + +# Create .desktop file +cat > "$PKG_DIR/usr/share/applications/android-file-handler.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=Android File Handler +Comment=Manage Android device files via ADB +Exec=/usr/local/bin/android-file-handler %U +Icon=android-file-handler +Terminal=false +Categories=Utility;Development; +StartupNotify=true +EOF +chmod 644 "$PKG_DIR/usr/share/applications/android-file-handler.desktop" + +# Copy icon if present in repo (fallback optional) +if [ -f "assets/icons/android-file-handler-256.png" ]; then + cp assets/icons/android-file-handler-256.png "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" + chmod 644 "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" +fi + +# Build the Debian package and include postinst script fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ ---architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ --p "dist/android-file-handler_${VERSION}_amd64.deb" -C dist android-file-handler \ No newline at end of file + --architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ + --after-install scripts/debian/postinst.sh \ + -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" usr/local/bin/android-file-handler usr/share/applications/android-file-handler.desktop usr/share/icons/hicolor/256x256/apps/android-file-handler.png \ No newline at end of file diff --git a/scripts/linux_postinst.sh b/scripts/linux_postinst.sh new file mode 100644 index 0000000..19020a5 --- /dev/null +++ b/scripts/linux_postinst.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# Post-installation script for android-file-handler +# - update desktop database if available +# - update icon cache if available +# - ensure binary is executable + +# Update desktop database if the utility exists +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database /usr/share/applications || true +fi + +# Update GTK icon cache for hicolor theme if utility exists +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + if [ -d /usr/share/icons/hicolor ]; then + gtk-update-icon-cache -t -f /usr/share/icons/hicolor || true + fi +fi + +# Ensure installed binary is executable +if [ -f /usr/local/bin/android-file-handler ]; then + chmod 0755 /usr/local/bin/android-file-handler || true +fi + +exit 0 diff --git a/scripts/spec_scripts/android-file-handler-linux.spec b/scripts/spec_scripts/android-file-handler-linux.spec index a5819d3..73f84c8 100644 --- a/scripts/spec_scripts/android-file-handler-linux.spec +++ b/scripts/spec_scripts/android-file-handler-linux.spec @@ -43,5 +43,4 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon='icon_media/robot_files_256.png', ) \ No newline at end of file diff --git a/scripts/spec_scripts/android-file-handler-windows.spec b/scripts/spec_scripts/android-file-handler-windows.spec index 1303195..f5bfa10 100644 --- a/scripts/spec_scripts/android-file-handler-windows.spec +++ b/scripts/spec_scripts/android-file-handler-windows.spec @@ -4,7 +4,7 @@ a = Analysis( ['src/main.py'], pathex=['src'], binaries=[], - datas=[('src/gui', 'gui')], + datas=[('src/gui', 'gui'), ('../scripts/windows/first_run_install.ps1', 'scripts/windows')], hiddenimports=[ 'gui', 'gui.file_browser', diff --git a/scripts/windows/first_run_install.ps1 b/scripts/windows/first_run_install.ps1 new file mode 100644 index 0000000..55c4c64 --- /dev/null +++ b/scripts/windows/first_run_install.ps1 @@ -0,0 +1,51 @@ +param( + [string]$ExePath = "$PSScriptRoot\..\..\dist\android-file-handler.exe", + [string]$IconPath = "$PSScriptRoot\..\..\assets\icons\android-file-handler.ico", + [string]$AppName = "Android File Handler" +) + +function Ensure-Elevated { + if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + # Relaunch the script with elevation + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "powershell.exe" + $psi.Arguments = "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" + $psi.Verb = "runas" + try { + [System.Diagnostics.Process]::Start($psi) | Out-Null + Exit 0 + } catch { + Write-Error "Elevation required to install to Program Files." + Exit 1 + } + } +} + +Ensure-Elevated + +$destDir = Join-Path ${env:ProgramFiles} $AppName +if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir | Out-Null } + +$resolvedExe = Resolve-Path -Path $ExePath -ErrorAction SilentlyContinue +if (-not $resolvedExe) { + Write-Error "Application executable not found at $ExePath" + Exit 1 +} + +Copy-Item -Path $resolvedExe -Destination (Join-Path $destDir (Split-Path $resolvedExe -Leaf)) -Force + +# Create Start Menu shortcut +$programs = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' +$appFolder = Join-Path $programs $AppName +if (-not (Test-Path $appFolder)) { New-Item -ItemType Directory -Path $appFolder | Out-Null } + +$shortcutPath = Join-Path $appFolder "$AppName.lnk" +$wsh = New-Object -ComObject WScript.Shell +$sc = $wsh.CreateShortcut($shortcutPath) +$sc.TargetPath = (Join-Path $destDir (Split-Path $resolvedExe -Leaf)) +$sc.WorkingDirectory = $destDir +if (Test-Path $IconPath) { $sc.IconLocation = Resolve-Path $IconPath } +$sc.Save() + +Write-Output "Installed $AppName to $destDir and created Start Menu shortcut." +Exit 0 diff --git a/src/gui/license_agreement.py b/src/gui/license_agreement.py index 98390fd..78f28ad 100644 --- a/src/gui/license_agreement.py +++ b/src/gui/license_agreement.py @@ -9,6 +9,9 @@ import tkinter as tk from tkinter import messagebox, scrolledtext import tempfile import stat +import subprocess +import sys +import os def get_license_file_path() -> str: @@ -104,12 +107,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" +def resource_path(relative_path: str) -> str: + """Return absolute path to resource for dev and frozen runs.""" + try: + if getattr(sys, "frozen", False): + base = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable)) + else: + base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return os.path.normpath(os.path.join(base, relative_path)) + except Exception: + return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)) + + +def run_windows_first_run_if_needed() -> None: + """If running on Windows and license not agreed, launch first-run installer script. + + Uses `resource_path` to locate the bundled PowerShell script in both dev and frozen modes. + """ + try: + if not sys.platform.startswith("win"): + return + + if check_license_agreement(): + return + + script_rel = os.path.join("scripts", "windows", "first_run_install.ps1") + script_path = resource_path(script_rel) + if not os.path.exists(script_path): + return + try: + subprocess.Popen(["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + pass + except Exception: + pass + + class LicenseAgreementFrame(tk.Frame): """License agreement UI frame that can be embedded in the main window.""" - + def __init__(self, parent, on_agree_callback): """Initialize the license agreement frame. - + Args: parent: Parent widget on_agree_callback: Function to call when user agrees to license diff --git a/src/main.py b/src/main.py index a9186e4..88a95c7 100644 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,9 @@ Simple entry point to launch the Android file transfer application. """ from gui.main_window import main +from gui.license_agreement import run_windows_first_run_if_needed if __name__ == "__main__": + run_windows_first_run_if_needed() main()