diff --git a/windows_xp_driver_ui/Makefile b/windows_xp_driver_ui/Makefile new file mode 100644 index 0000000..e106a7c --- /dev/null +++ b/windows_xp_driver_ui/Makefile @@ -0,0 +1,12 @@ +CC = i686-w64-mingw32-gcc +CFLAGS = -O2 -Wall -D_WIN32_WINNT=0x0501 -mwindows +LDFLAGS = -lwinhttp +TARGET = driver_xp.exe + +all: $(TARGET) + +$(TARGET): driver_xp.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f $(TARGET) diff --git a/windows_xp_driver_ui/README.md b/windows_xp_driver_ui/README.md new file mode 100644 index 0000000..7c6373b --- /dev/null +++ b/windows_xp_driver_ui/README.md @@ -0,0 +1,47 @@ +# Windows XP Driver UI + +This project is a native Win32 application built for Windows XP compatibility. +It avoids .NET and WinForms completely and targets a classic Windows API implementation instead. + +## Features + +- Login form for driver authentication +- Calls the existing auth API on 192.168.68.66:5000 +- Fetches the daily schedule from 192.168.68.66:5001 +- Works with Windows XP and older native Windows systems +- Uses the WinHTTP API, which is available on XP + +## Build + +```bash +make +``` + +This uses the MinGW cross-compiler targeting 32-bit Windows. + +## Runtime requirements + +The app expects the following services to be available: + +- auth_api on http://192.168.68.66:5000 +- transit_api on http://192.168.68.66:5001 + +The app intentionally points at the live network host instead of localhost so it works on a Windows XP machine connected to the same LAN. + +## Installer + +A custom Windows installer is included and generates a classic setup package for XP-era machines. + +```bash +makensis installer.nsi +``` + +This creates `TransitDriverXPSetup.exe`, which installs: + +- `driver_xp.exe` +- `README.md` +- Start menu shortcut +- Desktop shortcut +- Uninstaller entry + +The installer is designed for test deployment on a real Windows XP or older Windows machine compatible with native Win32 executables. diff --git a/windows_xp_driver_ui/TransitDriverXPInstaller.exe b/windows_xp_driver_ui/TransitDriverXPInstaller.exe new file mode 100644 index 0000000..9087967 Binary files /dev/null and b/windows_xp_driver_ui/TransitDriverXPInstaller.exe differ diff --git a/windows_xp_driver_ui/TransitDriverXPSetup.exe b/windows_xp_driver_ui/TransitDriverXPSetup.exe new file mode 100644 index 0000000..efcaf00 Binary files /dev/null and b/windows_xp_driver_ui/TransitDriverXPSetup.exe differ diff --git a/windows_xp_driver_ui/driver_xp.c b/windows_xp_driver_ui/driver_xp.c new file mode 100644 index 0000000..ff2b592 --- /dev/null +++ b/windows_xp_driver_ui/driver_xp.c @@ -0,0 +1,881 @@ +#define _WIN32_WINNT 0x0501 + +#include +#include +#include +#include +#include +#include + +#define ID_USERNAME 101 +#define ID_PASSWORD 102 +#define ID_LOGIN 103 +#define ID_REFRESH 104 +#define ID_OUTPUT 105 +#define ID_STATUS 106 +#define ID_DATE 107 +#define ID_ROUTES 108 +#define ID_STOPS 109 + +#define MAX_BUF 65535 +#define API_HOST "192.168.68.66" +#define AUTH_PORT 5000 +#define TRANSIT_PORT 5001 + +typedef struct { + HWND hwnd; + char token[512]; + char lastText[8192]; + int routeCount; + int routeLineIds[32]; + char routeLabels[32][256]; +} APPSTATE; + +static void SetStatusText(HWND hwnd, const char *text) +{ + SetWindowTextA(GetDlgItem(hwnd, ID_STATUS), text); +} + +static void SetOutputText(HWND hwnd, const char *text) +{ + SetWindowTextA(GetDlgItem(hwnd, ID_OUTPUT), text); +} + +static void TrimNewlines(char *text) +{ + size_t len = strlen(text); + while (len > 0 && (text[len - 1] == '\n' || text[len - 1] == '\r' || text[len - 1] == ' ')) { + text[len - 1] = '\0'; + len--; + } +} + +static void ExtractJsonFieldString(const char *json, + const char *field, + char *out, + size_t outSize) +{ + char pattern[128]; + const char *key; + const char *colon; + const char *value; + const char *end; + + out[0] = '\0'; + if (!json || !field || outSize == 0) { + return; + } + + snprintf(pattern, sizeof(pattern), "\"%s\"", field); + key = strstr(json, pattern); + if (!key) { + return; + } + + colon = strchr(key + strlen(pattern), ':'); + if (!colon) { + return; + } + + value = colon + 1; + while (*value == ' ' || *value == '\t' || *value == '\n' || *value == '\r') { + value++; + } + + if (*value == '"') { + const char *q1 = value + 1; + const char *q2 = strchr(q1, '"'); + size_t len; + + if (!q2) { + return; + } + + len = (size_t)(q2 - q1); + if (len >= outSize) { + len = outSize - 1; + } + memcpy(out, q1, len); + out[len] = '\0'; + return; + } + + end = value; + while (*end && *end != ',' && *end != '}' && *end != '\n' && *end != '\r') { + end++; + } + + if (end == value) { + return; + } + + size_t len = (size_t)(end - value); + while (len > 0 && (value[len - 1] == ' ' || value[len - 1] == '\t' || value[len - 1] == '\r' || value[len - 1] == '\n')) { + len--; + } + + if (len >= outSize) { + len = outSize - 1; + } + + memcpy(out, value, len); + out[len] = '\0'; +} + +static char *FindObjectEnd(char *start) +{ + int depth = 0; + char *p = start; + + while (*p) { + if (*p == '{') { + depth++; + } else if (*p == '}') { + depth--; + if (depth == 0) { + return p; + } + } + p++; + } + + return NULL; +} + +static void ExtractScheduleSummary(const char *json, APPSTATE *state, char *out, size_t outSize) +{ + const char *routesKey; + const char *p; + char routeSummary[2048]; + char summary[8192]; + int routeCount = 0; + + out[0] = '\0'; + summary[0] = '\0'; + routeSummary[0] = '\0'; + state->routeCount = 0; + SendMessage(GetDlgItem(state->hwnd, ID_ROUTES), LB_RESETCONTENT, 0, 0); + + routesKey = strstr(json, "\"routes\""); + if (!routesKey) { + snprintf(out, outSize, "No routes found for this date."); + return; + } + + p = strchr(routesKey, '['); + if (!p) { + snprintf(out, outSize, "No routes found for this date."); + return; + } + + p++; + while (*p) { + char *objStart = strchr((char *)p, '{'); + char *objEnd; + char routeText[2048]; + char routeLine[128]; + char routeStatus[128]; + char routeStart[128]; + char routeEnd[128]; + char routeStartStop[128]; + char routeEndStop[128]; + char routeLineId[32]; + char routeStartTime[128]; + + if (!objStart) { + break; + } + + objEnd = FindObjectEnd(objStart); + if (!objEnd) { + break; + } + + if ((size_t)(objEnd - objStart + 1) >= sizeof(routeText)) { + break; + } + + memcpy(routeText, objStart, objEnd - objStart + 1); + routeText[objEnd - objStart + 1] = '\0'; + + routeLine[0] = '\0'; + routeStatus[0] = '\0'; + routeStart[0] = '\0'; + routeEnd[0] = '\0'; + routeStartStop[0] = '\0'; + routeEndStop[0] = '\0'; + routeLineId[0] = '\0'; + routeStartTime[0] = '\0'; + + ExtractJsonFieldString(routeText, "lineNumber", routeLine, sizeof(routeLine)); + ExtractJsonFieldString(routeText, "startTime", routeStartTime, sizeof(routeStartTime)); + ExtractJsonFieldString(routeText, "id", routeLineId, sizeof(routeLineId)); + if (routeLine[0] == '\0') { + char busLine[1024]; + const char *busKey = strstr(routeText, "\"busLine\""); + if (busKey) { + const char *busStart = strchr(busKey, '{'); + if (busStart) { + char *busEnd = FindObjectEnd((char *)busStart); + if (busEnd) { + size_t busLen = (size_t)(busEnd - busStart + 1); + if (busLen < sizeof(busLine)) { + memcpy(busLine, busStart, busLen); + busLine[busLen] = '\0'; + ExtractJsonFieldString(busLine, "lineNumber", routeLine, sizeof(routeLine)); + ExtractJsonFieldString(busLine, "id", routeLineId, sizeof(routeLineId)); + ExtractJsonFieldString(busLine, "startStop", routeStartStop, sizeof(routeStartStop)); + ExtractJsonFieldString(busLine, "endStop", routeEndStop, sizeof(routeEndStop)); + } + } + } + } + } + + ExtractJsonFieldString(routeText, "status", routeStatus, sizeof(routeStatus)); + ExtractJsonFieldString(routeText, "startTime", routeStart, sizeof(routeStart)); + ExtractJsonFieldString(routeText, "endTime", routeEnd, sizeof(routeEnd)); + if (routeStartStop[0] == '\0') { + ExtractJsonFieldString(routeText, "startStop", routeStartStop, sizeof(routeStartStop)); + } + if (routeEndStop[0] == '\0') { + ExtractJsonFieldString(routeText, "endStop", routeEndStop, sizeof(routeEndStop)); + } + + if (routeLine[0] == '\0') { + snprintf(routeLine, sizeof(routeLine), "R%d", routeCount + 1); + } + if (routeStatus[0] == '\0') { + snprintf(routeStatus, sizeof(routeStatus), "planned"); + } + if (routeStartStop[0] == '\0') { + snprintf(routeStartStop, sizeof(routeStartStop), "N/A"); + } + if (routeEndStop[0] == '\0') { + snprintf(routeEndStop, sizeof(routeEndStop), "N/A"); + } + + routeCount++; + if (state->routeCount < 32) { + state->routeLineIds[state->routeCount] = atoi(routeLineId); + snprintf(state->routeLabels[state->routeCount], sizeof(state->routeLabels[state->routeCount]), + "Line %s %s %s", routeLine, routeStart, routeStatus); + SendMessageA(GetDlgItem(state->hwnd, ID_ROUTES), LB_ADDSTRING, 0, + (LPARAM)state->routeLabels[state->routeCount]); + state->routeCount++; + } + snprintf(routeSummary + strlen(routeSummary), sizeof(routeSummary) - strlen(routeSummary), + "Route %s: %s -> %s | %s | %s\r\n", + routeLine, + routeStartStop, + routeEndStop, + routeStart, + routeStatus); + + p = objEnd + 1; + while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',') { + p++; + } + + if (*p == ']' || *p == '\0') { + break; + } + } + + if (routeCount == 0) { + snprintf(out, outSize, "No routes found for this date."); + return; + } + + snprintf(summary, sizeof(summary), "Daily schedule (%d route%s)\r\n\r\n%s", + routeCount, routeCount == 1 ? "" : "s", + routeSummary); + + TrimNewlines(summary); + snprintf(out, outSize, "%s", summary); +} + +static void ExtractStopsSummary(const char *json, char *out, size_t outSize) +{ + const char *stopsKey = strstr(json, "\"stops\""); + const char *p; + char summary[4096]; + int count = 0; + + summary[0] = '\0'; + if (!stopsKey || !(p = strchr(stopsKey, '['))) { + snprintf(out, outSize, "No stop details were returned for this route."); + return; + } + + p++; + while (*p && *p != ']') { + char *start = strchr((char *)p, '{'); + char *end; + char item[1024]; + char order[32]; + char name[128]; + char eta[32]; + + if (!start || !(end = FindObjectEnd(start))) { + break; + } + if ((size_t)(end - start + 1) >= sizeof(item)) { + break; + } + + memcpy(item, start, end - start + 1); + item[end - start + 1] = '\0'; + ExtractJsonFieldString(item, "order", order, sizeof(order)); + ExtractJsonFieldString(item, "name", name, sizeof(name)); + ExtractJsonFieldString(item, "estimatedArrivalMinutes", eta, sizeof(eta)); + if (name[0] == '\0') { + snprintf(name, sizeof(name), "Unnamed stop"); + } + if (eta[0] == '\0') { + snprintf(eta, sizeof(eta), "-"); + } + + snprintf(summary + strlen(summary), sizeof(summary) - strlen(summary), + "%s. %s (arrival +%s min)\r\n", order, name, eta); + count++; + p = end + 1; + while (*p == ' ' || *p == '\r' || *p == '\n' || *p == '\t' || *p == ',') { + p++; + } + } + + if (count == 0) { + snprintf(out, outSize, "No stop details were returned for this route."); + return; + } + + snprintf(out, outSize, "Stop-by-stop timetable (%d stops)\r\n\r\n%s", count, summary); +} + +static void ExtractTokenFromJson(const char *json, char *outToken, size_t outSize) +{ + const char *tokenKey = strstr(json, "\"token\""); + const char *q1; + const char *q2; + + outToken[0] = '\0'; + if (!tokenKey) { + return; + } + + q1 = strchr(tokenKey, ':'); + if (!q1) { + return; + } + q1 = strchr(q1 + 1, '"'); + if (!q1) { + return; + } + q2 = strchr(q1 + 1, '"'); + if (!q2) { + return; + } + + q1++; + if ((size_t)(q2 - q1) >= outSize) { + strncpy(outToken, q1, outSize - 1); + outToken[outSize - 1] = '\0'; + return; + } + + memcpy(outToken, q1, q2 - q1); + outToken[q2 - q1] = '\0'; +} + +static void HttpErrorMessage(const char *context, DWORD error) +{ + char text[256]; + snprintf(text, sizeof(text), "%s (WinHTTP error %lu)", context, error); + MessageBoxA(NULL, text, "Driver app error", MB_ICONERROR | MB_OK); +} + +static int HttpRequestText(const char *method, + const char *url, + const char *body, + const char *authHeader, + char *response, + size_t responseSize) +{ + HINTERNET hSession = NULL; + HINTERNET hConnect = NULL; + HINTERNET hRequest = NULL; + wchar_t hostW[256]; + wchar_t pathW[512]; + wchar_t methodW[32]; + char host[256]; + char path[512]; + const char *src; + const char *slash; + const char *colon; + int port = 80; + DWORD flags = 0; + DWORD status = 0; + DWORD statusLen = sizeof(status); + DWORD bytesRead = 0; + DWORD totalRead = 0; + char buffer[MAX_BUF]; + int ok = 0; + + response[0] = '\0'; + + src = url; + if (strncmp(src, "http://", 7) == 0) { + src += 7; + } else if (strncmp(src, "https://", 8) == 0) { + src += 8; + } + + host[0] = '\0'; + path[0] = '/'; + path[1] = '\0'; + + slash = strchr(src, '/'); + colon = strchr(src, ':'); + + if (slash) { + size_t hostLen = (size_t)(slash - src); + if (hostLen >= sizeof(host)) { + hostLen = sizeof(host) - 1; + } + memcpy(host, src, hostLen); + host[hostLen] = '\0'; + snprintf(path, sizeof(path), "%.*s", (int)(strlen(slash)), slash); + } else if (colon) { + size_t hostLen = (size_t)(colon - src); + if (hostLen >= sizeof(host)) { + hostLen = sizeof(host) - 1; + } + memcpy(host, src, hostLen); + host[hostLen] = '\0'; + port = atoi(colon + 1); + snprintf(path, sizeof(path), "/"); + } else { + snprintf(host, sizeof(host), "%s", src); + snprintf(path, sizeof(path), "/"); + } + + if (colon && slash) { + size_t hostLen = (size_t)(colon - src); + if (hostLen >= sizeof(host)) { + hostLen = sizeof(host) - 1; + } + memcpy(host, src, hostLen); + host[hostLen] = '\0'; + port = atoi(colon + 1); + } + + if (host[0] == '\0') { + snprintf(host, sizeof(host), "192.168.68.66"); + } + + if (port == 0) { + port = 80; + } + + MultiByteToWideChar(CP_UTF8, 0, host, -1, hostW, 256); + MultiByteToWideChar(CP_UTF8, 0, path, -1, pathW, 512); + MultiByteToWideChar(CP_UTF8, 0, method, -1, methodW, 32); + + hSession = WinHttpOpen(L"TransitDriverXP/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + if (!hSession) { + HttpErrorMessage("Unable to open WinHTTP session", GetLastError()); + return 0; + } + + hConnect = WinHttpConnect(hSession, hostW, port, 0); + if (!hConnect) { + char hostText[256]; + char errorText[512]; + WideCharToMultiByte(CP_UTF8, 0, hostW, -1, hostText, sizeof(hostText), NULL, NULL); + snprintf(errorText, sizeof(errorText), "Unable to connect to %s:%d", hostText, port); + HttpErrorMessage(errorText, GetLastError()); + goto cleanup; + } + + hRequest = WinHttpOpenRequest(hConnect, + methodW, + pathW, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + 0); + if (!hRequest) { + HttpErrorMessage("Unable to open request", GetLastError()); + goto cleanup; + } + + if (body != NULL && body[0] != '\0') { + if (!WinHttpAddRequestHeaders(hRequest, + L"Content-Type: application/json\r\n", + -1, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + HttpErrorMessage("Unable to add JSON content type", GetLastError()); + goto cleanup; + } + } + + if (authHeader && authHeader[0] != '\0') { + wchar_t wHeader[512]; + MultiByteToWideChar(CP_UTF8, 0, authHeader, -1, wHeader, 512); + if (!WinHttpAddRequestHeaders(hRequest, + wHeader, + -1, + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) { + HttpErrorMessage("Unable to add authorization header", GetLastError()); + goto cleanup; + } + } + + if (!WinHttpSendRequest(hRequest, + WINHTTP_NO_ADDITIONAL_HEADERS, + 0, + (LPVOID)body, + body ? (DWORD)strlen(body) : 0, + body ? (DWORD)strlen(body) : 0, + 0)) { + HttpErrorMessage("WinHTTP send request failed", GetLastError()); + goto cleanup; + } + + if (!WinHttpReceiveResponse(hRequest, NULL)) { + HttpErrorMessage("WinHTTP receive response failed", GetLastError()); + goto cleanup; + } + + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status, + &statusLen, + WINHTTP_NO_HEADER_INDEX)) { + HttpErrorMessage("Unable to query HTTP status", GetLastError()); + goto cleanup; + } + + if (status >= 400) { + snprintf(response, responseSize, "HTTP error %lu", status); + ok = 0; + goto cleanup; + } + + while (WinHttpQueryDataAvailable(hRequest, &bytesRead) && bytesRead > 0) { + DWORD actual = 0; + if (!WinHttpReadData(hRequest, buffer, sizeof(buffer) - 1, &actual)) { + HttpErrorMessage("Unable to read response data", GetLastError()); + goto cleanup; + } + if (actual == 0) { + break; + } + buffer[actual] = '\0'; + if (totalRead + actual + 1 < responseSize) { + memcpy(response + totalRead, buffer, actual); + totalRead += actual; + response[totalRead] = '\0'; + } + } + + ok = 1; + +cleanup: + if (hRequest) { + WinHttpCloseHandle(hRequest); + } + if (hConnect) { + WinHttpCloseHandle(hConnect); + } + if (hSession) { + WinHttpCloseHandle(hSession); + } + + return ok; +} + +static void PerformLogin(HWND hwnd, APPSTATE *state) +{ + char userCode[128]; + char password[128]; + char payload[512]; + char response[MAX_BUF]; + char token[256]; + + GetWindowTextA(GetDlgItem(hwnd, ID_USERNAME), userCode, sizeof(userCode)); + GetWindowTextA(GetDlgItem(hwnd, ID_PASSWORD), password, sizeof(password)); + + if (userCode[0] == '\0' || password[0] == '\0') { + SetStatusText(hwnd, "Enter user code and password."); + return; + } + + snprintf(payload, sizeof(payload), + "{\"userCode\":\"%s\",\"password\":\"%s\"}", + userCode, password); + + SetStatusText(hwnd, "Logging in..."); + if (!HttpRequestText("POST", "http://192.168.68.66:5000/login", payload, NULL, response, sizeof(response))) { + SetOutputText(hwnd, "Login failed. Check that auth_api is running on 192.168.68.66:5000."); + SetStatusText(hwnd, "Login failed"); + return; + } + + ExtractTokenFromJson(response, token, sizeof(token)); + if (token[0] == '\0') { + SetOutputText(hwnd, response); + SetStatusText(hwnd, "No token returned"); + return; + } + + strncpy(state->token, token, sizeof(state->token) - 1); + state->token[sizeof(state->token) - 1] = '\0'; + snprintf(state->lastText, sizeof(state->lastText), "Login succeeded. Driver session ready.\r\nUse Refresh schedule to load the route list."); + SetOutputText(hwnd, state->lastText); + SetStatusText(hwnd, "Connected"); +} + +static void PerformRefresh(HWND hwnd, APPSTATE *state) +{ + char response[MAX_BUF]; + char authHeader[512]; + char date[32]; + char scheduleUrl[256]; + + if (state->token[0] == '\0') { + SetStatusText(hwnd, "Login first."); + return; + } + + snprintf(authHeader, sizeof(authHeader), "Authorization: Bearer %s\r\n", state->token); + GetWindowTextA(GetDlgItem(hwnd, ID_DATE), date, sizeof(date)); + if (date[0] == '\0') { + snprintf(date, sizeof(date), "2026-09-17"); + } + snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/schedule/daily?date=%s", date); + SetStatusText(hwnd, "Loading schedule..."); + + if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) { + SetOutputText(hwnd, "Unable to load daily schedule. Check that transit_api is running on 192.168.68.66:5001."); + SetStatusText(hwnd, "Schedule load failed"); + return; + } + + ExtractScheduleSummary(response, state, state->lastText, sizeof(state->lastText)); + SetOutputText(hwnd, state->lastText); + SetStatusText(hwnd, "Schedule loaded"); +} + +static void PerformLoadStops(HWND hwnd, APPSTATE *state) +{ + char response[MAX_BUF]; + char authHeader[512]; + char stopsUrl[256]; + LRESULT selected; + int lineId; + + if (state->token[0] == '\0') { + SetStatusText(hwnd, "Login first."); + return; + } + + selected = SendMessage(GetDlgItem(hwnd, ID_ROUTES), LB_GETCURSEL, 0, 0); + if (selected == LB_ERR || selected >= state->routeCount) { + SetStatusText(hwnd, "Select a route first."); + return; + } + + lineId = state->routeLineIds[selected]; + if (lineId <= 0) { + SetOutputText(hwnd, "This schedule does not contain a valid bus line ID."); + SetStatusText(hwnd, "Stop details unavailable"); + return; + } + + snprintf(authHeader, sizeof(authHeader), "Authorization: Bearer %s\r\n", state->token); + snprintf(stopsUrl, sizeof(stopsUrl), "http://192.168.68.66:5001/bus-lines/%d/stops", lineId); + SetStatusText(hwnd, "Loading stop details..."); + + if (!HttpRequestText("GET", stopsUrl, NULL, authHeader, response, sizeof(response))) { + SetOutputText(hwnd, "Unable to load stops for this route. Check the transit API connection."); + SetStatusText(hwnd, "Stop details failed"); + return; + } + + ExtractStopsSummary(response, state->lastText, sizeof(state->lastText)); + SetOutputText(hwnd, state->lastText); + SetStatusText(hwnd, "Stop details loaded"); +} + +static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + static APPSTATE *state = NULL; + HWND hUser, hPassword, hLogin, hRefresh, hOutput, hStatus; + + switch (msg) { + case WM_CREATE: + { + LPCREATESTRUCT cs = (LPCREATESTRUCT)lParam; + state = (APPSTATE *)calloc(1, sizeof(APPSTATE)); + state->hwnd = hwnd; + cs->lpCreateParams = state; + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)state); + + CreateWindowExA(0, "STATIC", "TRANSIT DRIVER CONSOLE", + WS_CHILD | WS_VISIBLE, + 20, 10, 330, 24, + hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(0, "STATIC", "Schedule date (YYYY-MM-DD)", + WS_CHILD | WS_VISIBLE, + 20, 38, 150, 20, + hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(0, "EDIT", "2026-09-17", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL, + 175, 36, 130, 24, + hwnd, (HMENU)ID_DATE, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(0, "STATIC", "User code", + WS_CHILD | WS_VISIBLE, + 20, 72, 90, 20, + hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hUser = CreateWindowExA(0, "EDIT", "", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL, + 120, 70, 180, 24, + hwnd, (HMENU)ID_USERNAME, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + SetFocus(hUser); + + CreateWindowExA(0, "STATIC", "Password", + WS_CHILD | WS_VISIBLE, + 20, 104, 90, 20, + hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hPassword = CreateWindowExA(0, "EDIT", "", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_PASSWORD | ES_AUTOHSCROLL, + 120, 102, 180, 24, + hwnd, (HMENU)ID_PASSWORD, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hLogin = CreateWindowExA(0, "BUTTON", "Login", + WS_CHILD | WS_VISIBLE | WS_TABSTOP, + 120, 138, 90, 30, + hwnd, (HMENU)ID_LOGIN, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hRefresh = CreateWindowExA(0, "BUTTON", "Refresh schedule", + WS_CHILD | WS_VISIBLE | WS_TABSTOP, + 220, 138, 130, 30, + hwnd, (HMENU)ID_REFRESH, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hStatus = CreateWindowExA(0, "STATIC", "Idle", + WS_CHILD | WS_VISIBLE, + 20, 178, 500, 22, + hwnd, (HMENU)ID_STATUS, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(0, "STATIC", "TODAY'S ROUTES", + WS_CHILD | WS_VISIBLE, + 20, 208, 200, 20, + hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(WS_EX_CLIENTEDGE, "LISTBOX", "", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | LBS_NOTIFY | WS_VSCROLL, + 20, 230, 500, 105, + hwnd, (HMENU)ID_ROUTES, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + hOutput = CreateWindowExA(WS_EX_CLIENTEDGE, "EDIT", "", + WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_READONLY, + 20, 345, 500, 190, + hwnd, (HMENU)ID_OUTPUT, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + CreateWindowExA(0, "BUTTON", "Show selected route stops", + WS_CHILD | WS_VISIBLE | WS_TABSTOP, + 20, 545, 190, 30, + hwnd, (HMENU)ID_STOPS, ((LPCREATESTRUCT)lParam)->hInstance, NULL); + + SetWindowTextA(hOutput, "Driver console ready.\r\nLog in, choose a date, and refresh the schedule."); + return 0; + } + + case WM_COMMAND: + if (LOWORD(wParam) == ID_LOGIN && HIWORD(wParam) == BN_CLICKED) { + state = (APPSTATE *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + PerformLogin(hwnd, state); + return 0; + } + + if (LOWORD(wParam) == ID_REFRESH && HIWORD(wParam) == BN_CLICKED) { + state = (APPSTATE *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + PerformRefresh(hwnd, state); + return 0; + } + + if (LOWORD(wParam) == ID_STOPS && HIWORD(wParam) == BN_CLICKED) { + state = (APPSTATE *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + PerformLoadStops(hwnd, state); + return 0; + } + break; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) +{ + WNDCLASSA wc; + HWND hwnd; + MSG msg; + + memset(&wc, 0, sizeof(wc)); + wc.lpfnWndProc = WndProc; + wc.hInstance = hInstance; + wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1); + wc.lpszClassName = "TransitDriverXPClass"; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.style = CS_HREDRAW | CS_VREDRAW; + + if (!RegisterClassA(&wc)) { + MessageBoxA(NULL, "Failed to register window class.", "XP Driver App", MB_ICONERROR | MB_OK); + return 1; + } + + hwnd = CreateWindowExA(0, + "TransitDriverXPClass", + "Transit Driver XP", + WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, + CW_USEDEFAULT, + 560, + 640, + NULL, + NULL, + hInstance, + NULL); + + if (!hwnd) { + MessageBoxA(NULL, "Failed to create window.", "XP Driver App", MB_ICONERROR | MB_OK); + return 1; + } + + ShowWindow(hwnd, nCmdShow); + UpdateWindow(hwnd); + + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return (int)msg.wParam; +} diff --git a/windows_xp_driver_ui/driver_xp.exe b/windows_xp_driver_ui/driver_xp.exe new file mode 100755 index 0000000..c5a6add Binary files /dev/null and b/windows_xp_driver_ui/driver_xp.exe differ diff --git a/windows_xp_driver_ui/installer.nsi b/windows_xp_driver_ui/installer.nsi new file mode 100644 index 0000000..a53ac73 --- /dev/null +++ b/windows_xp_driver_ui/installer.nsi @@ -0,0 +1,52 @@ +; Transit Driver XP custom installer +!include "MUI2.nsh" + +!define APPNAME "Transit Driver XP" +!define COMPANYNAME "Transit Project" +!define DESCRIPTION "Native Windows XP-compatible driver app" +!define VERSIONMAJOR 1 +!define VERSIONMINOR 0 +!define VERSIONBUILD 0 + +Name "${APPNAME}" +OutFile "TransitDriverXPSetup.exe" +InstallDir "$PROGRAMFILES\TransitDriverXP" +InstallDirRegKey HKCU "Software\TransitDriverXP" "InstallDir" +RequestExecutionLevel admin + +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_LANGUAGE "English" + +Section "Install" + SetOutPath "$INSTDIR" + File "driver_xp.exe" + File "README.md" + + WriteRegStr HKCU "Software\TransitDriverXP" "InstallDir" "$INSTDIR" + WriteRegStr HKCU "Software\TransitDriverXP" "DisplayName" "${APPNAME}" + + CreateDirectory "$SMPROGRAMS\TransitDriverXP" + CreateShortCut "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe" + CreateShortCut "$DESKTOP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe" + + WriteUninstaller "$INSTDIR\Uninstall.exe" + CreateShortCut "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" "$INSTDIR\Uninstall.exe" + + ; launch after install + ExecShell "open" "$INSTDIR\driver_xp.exe" +SectionEnd + +Section "Uninstall" + Delete "$DESKTOP\Transit Driver XP.lnk" + Delete "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" + Delete "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" + RMDir "$SMPROGRAMS\TransitDriverXP" + + Delete "$INSTDIR\driver_xp.exe" + Delete "$INSTDIR\README.md" + Delete "$INSTDIR\Uninstall.exe" + RMDir "$INSTDIR" + + DeleteRegKey HKCU "Software\TransitDriverXP" +SectionEnd