diff --git a/README.md b/README.md index 6b47ef0..1367c83 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,9 @@ Fixed - Fixed Device Simulator Add Device and Remove Device interactions inside embedded webviews. - Fixed Send To Mobile browser preview spending the View button's initial tap as a premature playable click-through while preserving real user-initiated redirects. ``` + +**v0.2.2** +``` +Added + - Added automatic update checking to the VS Code extension and standalone app. +``` diff --git a/dist/hpl-toolbox-0.2.1.exe b/dist/hpl-toolbox-0.2.2.exe similarity index 69% rename from dist/hpl-toolbox-0.2.1.exe rename to dist/hpl-toolbox-0.2.2.exe index ca94fe2..c2fb82e 100644 Binary files a/dist/hpl-toolbox-0.2.1.exe and b/dist/hpl-toolbox-0.2.2.exe differ diff --git a/dist/hpl-toolbox-0.2.1.vsix b/dist/hpl-toolbox-0.2.2.vsix similarity index 99% rename from dist/hpl-toolbox-0.2.1.vsix rename to dist/hpl-toolbox-0.2.2.vsix index 2431134..ceba8f7 100644 Binary files a/dist/hpl-toolbox-0.2.1.vsix and b/dist/hpl-toolbox-0.2.2.vsix differ diff --git a/package.json b/package.json index 2ea5c6b..1e91c05 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hpl-toolbox", "displayName": "HPL Toolbox", "description": "Bundled tools: PLEC Upload, AppLovin Playable Preview, Base64 Scanner, MRAID Checker. Local HTML Host.", - "version": "0.2.1", + "version": "0.2.2", "publisher": "hesukastro", "license": "UNLICENSED", "repository": { @@ -51,6 +51,10 @@ { "command": "hplToolbox.openDeviceSimulator", "title": "HPL Toolbox: Open Device Simulator" + }, + { + "command": "hplToolbox.checkForUpdates", + "title": "HPL Toolbox: Check for Updates" } ], "viewsContainers": { diff --git a/src/extension.ts b/src/extension.ts index aa47ed6..cff5b84 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import { openPlayworksConverter } from './tools/playworksConverter'; import { openMintegralChecker } from './tools/mintegralChecker'; import { openChangelog } from './changelogView'; import { openDeviceSimulator } from './tools/deviceSimulator'; +import { checkForUpdates } from './updateChecker'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( @@ -21,8 +22,11 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('hplToolbox.openMintegralChecker', () => openMintegralChecker(context)), vscode.commands.registerCommand('hplToolbox.openDeviceSimulator', () => openDeviceSimulator(context)), vscode.commands.registerCommand('hplToolbox.openChangelog', () => openChangelog(context)), + vscode.commands.registerCommand('hplToolbox.checkForUpdates', () => checkForUpdates(context)), vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context)) ); + + void checkForUpdates(context); } export function deactivate() {} diff --git a/src/launcherView.ts b/src/launcherView.ts index 82df3fb..589ed2f 100644 --- a/src/launcherView.ts +++ b/src/launcherView.ts @@ -62,6 +62,8 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider { vscode.commands.executeCommand(msg.command); } else if (msg?.type === 'openChangelog') { vscode.commands.executeCommand('hplToolbox.openChangelog'); + } else if (msg?.type === 'checkForUpdates') { + vscode.commands.executeCommand('hplToolbox.checkForUpdates'); } else if (msg?.type === 'setBetaToolsEnabled' && typeof msg.enabled === 'boolean') { await this.context.globalState.update(BETA_TOOLS_ENABLED_KEY, msg.enabled); view.webview.html = getHtml(version, msg.enabled, tools, this.context.globalState.get(TOOL_ORDER_KEY, [])); @@ -204,6 +206,7 @@ function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefiniti opacity: 0.75; text-align: center; white-space: nowrap; + justify-self: center; } .changelog-link { justify-self: end; @@ -217,7 +220,7 @@ ${toolButtons} `; diff --git a/src/updateChecker.ts b/src/updateChecker.ts new file mode 100644 index 0000000..0fdf2da --- /dev/null +++ b/src/updateChecker.ts @@ -0,0 +1,80 @@ +import * as vscode from 'vscode'; + +const REMOTE_PACKAGE_JSON_URL = 'https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox/raw/branch/main/package.json'; + +interface RemotePackageJson { + version?: unknown; +} + +export async function checkForUpdates(context: vscode.ExtensionContext): Promise { + const currentVersion = String(context.extension.packageJSON.version ?? '').trim(); + + if (!currentVersion) { + vscode.window.showWarningMessage('HPL Toolbox could not read the installed version.'); + return; + } + + try { + const remoteVersion = await fetchRemoteVersion(); + const comparison = compareVersions(remoteVersion, currentVersion); + + if (comparison > 0) { + const action = await vscode.window.showInformationMessage( + `HPL Toolbox update available: ${remoteVersion} (installed: ${currentVersion}).`, + 'Open Repository' + ); + + if (action === 'Open Repository') { + await vscode.env.openExternal(vscode.Uri.parse('https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox')); + } + return; + } + + vscode.window.showInformationMessage(`HPL Toolbox is up to date (${currentVersion}).`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + vscode.window.showWarningMessage(`HPL Toolbox update check failed: ${message}`); + } +} + +async function fetchRemoteVersion(): Promise { + const response = await fetch(REMOTE_PACKAGE_JSON_URL, { + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`remote package.json returned HTTP ${response.status}`); + } + + const packageJson = await response.json() as RemotePackageJson; + if (typeof packageJson.version !== 'string' || !packageJson.version.trim()) { + throw new Error('remote package.json is missing a version'); + } + + return packageJson.version.trim(); +} + +function compareVersions(left: string, right: string): number { + const leftParts = parseVersion(left); + const rightParts = parseVersion(right); + const length = Math.max(leftParts.length, rightParts.length); + + for (let index = 0; index < length; index += 1) { + const leftPart = leftParts[index] ?? 0; + const rightPart = rightParts[index] ?? 0; + + if (leftPart > rightPart) return 1; + if (leftPart < rightPart) return -1; + } + + return 0; +} + +function parseVersion(version: string): number[] { + return version + .split(/[.-]/) + .map(part => Number.parseInt(part, 10)) + .filter(part => Number.isFinite(part)); +} diff --git a/standalone/layout.go b/standalone/layout.go index 00eabec..2b75012 100644 --- a/standalone/layout.go +++ b/standalone/layout.go @@ -159,6 +159,55 @@ const SharedCSS = ` .app-footer-version { opacity: 0.75; text-align: center; white-space: nowrap; } .app-footer-link { justify-self: end; padding: 0; background: transparent; color: #a7a7a7; border: 0; cursor: pointer; font: inherit; opacity: 0.72; } .app-footer-link:hover { background: transparent; opacity: 1; text-decoration: underline; } + .app-footer-version-button { justify-self: center; padding: 0; background: transparent; color: #a7a7a7; border: 0; cursor: pointer; font: inherit; opacity: 0.75; text-align: center; white-space: nowrap; } + .app-footer-version-button:hover { background: transparent; opacity: 1; text-decoration: underline; } + .toast-box { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 50; + max-width: min(420px, calc(100vw - 36px)); + padding: 10px 12px; + display: none; + align-items: center; + gap: 10px; + color: #ddd; + background: #252526; + border: 1px solid #444; + border-left: 3px solid #007fd4; + border-radius: 5px; + box-shadow: 0 10px 28px rgba(0,0,0,0.35); + font-size: 12px; + line-height: 1.35; + } + .toast-box.is-visible { display: flex; } + .toast-box.is-update { border-left-color: #ffd580; } + .toast-box.is-current { border-left-color: #89d185; } + .toast-box.is-error { border-left-color: #f48771; } + .toast-message { min-width: 0; flex: 1; overflow-wrap: anywhere; } + .toast-action { + flex: 0 0 auto; + padding: 4px 8px; + background: #0e639c; + color: #fff; + border: 0; + border-radius: 2px; + font-size: 12px; + } + .toast-close { + flex: 0 0 auto; + width: 22px; + height: 22px; + padding: 0; + display: grid; + place-items: center; + background: transparent; + color: #a7a7a7; + border: 0; + font-size: 16px; + line-height: 1; + } + .toast-close:hover { background: rgba(255,255,255,0.08); color: #fff; } body.sidebar-collapsed .sidebar { align-items: stretch; } body.sidebar-collapsed .sidebar-title, body.sidebar-collapsed .nav-text, @@ -539,10 +588,15 @@ func Page(activePath, title, body string) string {
` + body + `
+
+ + + +
` } diff --git a/standalone/main.go b/standalone/main.go index 8ad9b09..75edf1e 100644 --- a/standalone/main.go +++ b/standalone/main.go @@ -123,6 +123,7 @@ func buildMux() *http.ServeMux { mux.HandleFunc("POST /api/open", OpenEndpoint) mux.HandleFunc("POST /api/focus", FocusEndpoint) mux.HandleFunc("POST /api/betaTools", BetaToolsEndpoint) + mux.HandleFunc("GET /api/update/check", UpdateCheckEndpoint) mux.HandleFunc("POST /api/drop/resolve", DropResolveEndpoint) // PLEC diff --git a/standalone/update_checker.go b/standalone/update_checker.go new file mode 100644 index 0000000..e2a1b31 --- /dev/null +++ b/standalone/update_checker.go @@ -0,0 +1,127 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +const remotePackageJSONURL = "https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox/raw/branch/main/package.json" +const updateRepositoryURL = "https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox" + +type updateCheckResponse struct { + OK bool `json:"ok"` + Status string `json:"status"` + CurrentVersion string `json:"currentVersion"` + RemoteVersion string `json:"remoteVersion,omitempty"` + Message string `json:"message"` + RepositoryURL string `json:"repositoryUrl,omitempty"` +} + +func UpdateCheckEndpoint(w http.ResponseWriter, r *http.Request) { + remoteVersion, err := fetchRemoteVersion() + if err != nil { + writeJSON(w, updateCheckResponse{ + OK: false, + Status: "error", + CurrentVersion: AppVersion, + Message: "HPL Toolbox update check failed: " + err.Error(), + }) + return + } + + comparison := compareVersions(remoteVersion, AppVersion) + if comparison > 0 { + writeJSON(w, updateCheckResponse{ + OK: true, + Status: "update", + CurrentVersion: AppVersion, + RemoteVersion: remoteVersion, + Message: fmt.Sprintf("HPL Toolbox update available: %s (installed: %s).", remoteVersion, AppVersion), + RepositoryURL: updateRepositoryURL, + }) + return + } + + writeJSON(w, updateCheckResponse{ + OK: true, + Status: "current", + CurrentVersion: AppVersion, + RemoteVersion: remoteVersion, + Message: fmt.Sprintf("HPL Toolbox is up to date (%s).", AppVersion), + }) +} + +func fetchRemoteVersion() (string, error) { + client := http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(http.MethodGet, remotePackageJSONURL, nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("remote package.json returned HTTP %d", resp.StatusCode) + } + + var packageJSON struct { + Version string `json:"version"` + } + if err := json.NewDecoder(resp.Body).Decode(&packageJSON); err != nil { + return "", err + } + if strings.TrimSpace(packageJSON.Version) == "" { + return "", fmt.Errorf("remote package.json is missing a version") + } + + return strings.TrimSpace(packageJSON.Version), nil +} + +func compareVersions(left, right string) int { + leftParts := parseVersionParts(left) + rightParts := parseVersionParts(right) + length := len(leftParts) + if len(rightParts) > length { + length = len(rightParts) + } + for i := 0; i < length; i++ { + leftPart := 0 + rightPart := 0 + if i < len(leftParts) { + leftPart = leftParts[i] + } + if i < len(rightParts) { + rightPart = rightParts[i] + } + if leftPart > rightPart { + return 1 + } + if leftPart < rightPart { + return -1 + } + } + return 0 +} + +func parseVersionParts(version string) []int { + fields := strings.FieldsFunc(version, func(r rune) bool { + return r == '.' || r == '-' + }) + parts := make([]int, 0, len(fields)) + for _, field := range fields { + part, err := strconv.Atoi(field) + if err == nil { + parts = append(parts, part) + } + } + return parts +}