diff --git a/starpilot/system/the_pond/assets/components/tools/theme_maker.js b/starpilot/system/the_pond/assets/components/tools/theme_maker.js index 488189aa..91eabca6 100644 --- a/starpilot/system/the_pond/assets/components/tools/theme_maker.js +++ b/starpilot/system/the_pond/assets/components/tools/theme_maker.js @@ -113,6 +113,48 @@ const state = reactive({ let draggedIndex = -1; let dropIndex = -1; +const getHttpErrorMessage = (response) => { + const statusText = response.statusText ? ` ${response.statusText}` : ""; + return `Request failed (${response.status}${statusText})`; +}; + +const readResponsePayload = async (response) => { + const contentType = String(response.headers.get("content-type") || "").toLowerCase(); + const text = await response.text(); + const trimmed = text.trim(); + + if (contentType.includes("application/json")) { + try { + return trimmed ? JSON.parse(trimmed) : {}; + } catch {} + } + + if (!trimmed || trimmed.startsWith("<")) { + return {}; + } + + try { + return JSON.parse(trimmed); + } catch { + return { message: trimmed, error: trimmed }; + } +}; + +const getResponseMessage = (response, payload = {}, fallbackMessage = "") => { + return payload.error || payload.message || fallbackMessage || getHttpErrorMessage(response); +}; + +const fetchFileAsFile = async (url, filename, errorMessage) => { + const response = await fetch(url); + if (!response.ok) { + const payload = await readResponsePayload(response); + throw new Error(getResponseMessage(response, payload, errorMessage)); + } + + const blob = await response.blob(); + return new File([blob], filename, { type: blob.type }); +}; + function handleDragStart(e, index) { draggedIndex = index; e.dataTransfer.effectAllowed = "move"; @@ -590,8 +632,10 @@ export function ThemeMaker() { const performApiAction = async (url, options, successMessage, errorMessage) => { try { const response = await fetch(url, options); - const result = await response.json(); - const message = result.message || (response.ok ? successMessage : errorMessage); + const result = await readResponsePayload(response); + const message = response.ok + ? (result.message || successMessage) + : getResponseMessage(response, result, errorMessage); if (message) { showSnackbar(message, response.ok ? "success" : "error"); } @@ -699,7 +743,11 @@ export function ThemeMaker() { const manageThemes = async () => { const response = await fetch("/api/themes/list"); - const data = await response.json(); + const data = await readResponsePayload(response); + if (!response.ok) { + showSnackbar(getResponseMessage(response, data, "Failed to load themes."), "error"); + return; + } state.themes = (data.themes || []).map(t => ({ ...t, localHasColors: !!t.hasColors, @@ -866,25 +914,22 @@ export function ThemeMaker() { try { const response = await fetch(`/api/themes/load/${theme.path}?type=${theme.type}`); - const data = await response.json(); + const data = await readResponsePayload(response); + if (!response.ok) { + throw new Error(getResponseMessage(response, data, "Failed to load theme asset.")); + } - const fetchAndStoreFile = async (assetPath, key, subkey = null, type = "image", assetGroup = "") => { + const fetchAndStoreFile = async (assetPath, key, subkey = null, type = "image") => { if (!assetPath) return; - try { - const url = `/api/themes/asset/${theme.path}/${assetPath}?type=${theme.type}`; - const fileResponse = await fetch(url); - const blob = await fileResponse.blob(); - const filename = assetPath.split("/").pop(); - const file = new File([blob], filename, { type: blob.type }); + const url = `/api/themes/asset/${theme.path}/${assetPath}?type=${theme.type}`; + const filename = assetPath.split("/").pop(); + const file = await fetchFileAsFile(url, filename, `Failed to load ${assetType.replace("_", " ")} from "${theme.name}".`); - const store = type === "image" ? fileStore.images : fileStore.sounds; - if (subkey) { - store[key][subkey] = file; - } else { - store[key] = file; - } - } catch (err) { - console.error(`Failed to load file from ${assetPath}`, err); + const store = type === "image" ? fileStore.images : fileStore.sounds; + if (subkey) { + store[key][subkey] = file; + } else { + store[key] = file; } }; @@ -895,35 +940,32 @@ export function ThemeMaker() { if (assetType === "distance_icons" && data.images.distanceIcons) { for (const [subkey, asset] of Object.entries(data.images.distanceIcons)) { state.imageFileNames.distanceIcons[subkey] = theme.name; - await fetchAndStoreFile(asset.path, "distanceIcons", subkey, "image", "distance_icons"); + await fetchAndStoreFile(asset.path, "distanceIcons", subkey, "image"); } } if (assetType === "icons" && (data.images.homeButton || data.images.settingsButton)) { if (data.images.homeButton) { state.imageFileNames.homeButton = theme.name; - await fetchAndStoreFile(data.images.homeButton.path, "homeButton", null, "image", "icons"); + await fetchAndStoreFile(data.images.homeButton.path, "homeButton", null, "image"); } if (data.images.settingsButton) { state.imageFileNames.settingsButton = theme.name; - await fetchAndStoreFile(data.images.settingsButton.path, "settingsButton", null, "image", "icons"); + await fetchAndStoreFile(data.images.settingsButton.path, "settingsButton", null, "image"); } } if (assetType === "steering_wheel" && data.images.steeringWheel?.path) { const url = `/api/themes/asset/${theme.path}/${data.images.steeringWheel.path}?type=${theme.type}`; - const fileResponse = await fetch(url); - const blob = await fileResponse.blob(); const filename = data.images.steeringWheel.filename || theme.path; - const file = new File([blob], filename, { type: blob.type }); - fileStore.images.steeringWheel = file; + fileStore.images.steeringWheel = await fetchFileAsFile(url, filename, `Failed to load steering wheel from "${theme.name}".`); state.imageFileNames.steeringWheel = theme.name; } if (assetType === "sounds" && Object.keys(data.sounds).length) { for (const [key, asset] of Object.entries(data.sounds)) { state.soundFileNames[key] = theme.name; - await fetchAndStoreFile(asset.path, key, null, "audio", "sounds"); + await fetchAndStoreFile(asset.path, key, null, "audio"); } } @@ -936,10 +978,10 @@ export function ThemeMaker() { state.imageFileNames.turnSignalBlindspot = data.images.turnSignalBlindspot?.filename ? theme.name : ""; if (data.images.turnSignal) { - await fetchAndStoreFile(data.images.turnSignal.path, "turnSignal", null, "image", "signals"); + await fetchAndStoreFile(data.images.turnSignal.path, "turnSignal", null, "image"); } if (data.images.turnSignalBlindspot) { - await fetchAndStoreFile(data.images.turnSignalBlindspot.path, "turnSignalBlindspot", null, "image", "signals"); + await fetchAndStoreFile(data.images.turnSignalBlindspot.path, "turnSignalBlindspot", null, "image"); } state.sequentialImages = data.sequentialImages || []; @@ -949,10 +991,7 @@ export function ThemeMaker() { state.imageFileNames.turnSignal = theme.name; for (const img of data.sequentialImages) { const url = `/api/themes/asset/${theme.path}/signals/${img}?type=${theme.type}`; - const fileResponse = await fetch(url); - const blob = await fileResponse.blob(); - const file = new File([blob], img, { type: blob.type }); - fileStore.sequentialFiles.push(file); + fileStore.sequentialFiles.push(await fetchFileAsFile(url, img, `Failed to load turn signal frames from "${theme.name}".`)); } } } @@ -960,7 +999,7 @@ export function ThemeMaker() { showSnackbar(`Loaded ${assetType.replace("_", " ")} from "${theme.name}"!`); } catch (err) { console.error("Failed to load theme asset:", err); - showSnackbar("Failed to load theme asset.", "error"); + showSnackbar(err.message || "Failed to load theme asset.", "error"); } finally { state.isLoadingAsset = false; } @@ -1035,13 +1074,13 @@ export function ThemeMaker() { : `&component=${state.activeTab === "turn_signals" ? "signals" : state.activeTab}`; const response = await fetch(`/api/themes/delete/${theme.path}?type=${theme.type}${component}`, { method: "DELETE" }); - const result = await response.json(); + const result = await readResponsePayload(response); if (response.ok) { - showSnackbar(result.message, "success"); + showSnackbar(getResponseMessage(response, result, "Theme deleted."), "success"); deleteThemeAndRestoreDownloadables(theme.name); manageThemes(); } else { - showSnackbar(result.message, "error"); + showSnackbar(getResponseMessage(response, result, "Failed to delete theme."), "error"); } state.showDeleteConfirmModal = false; state.themeToDelete = null; diff --git a/starpilot/system/the_pond/the_pond.py b/starpilot/system/the_pond/the_pond.py index a72384b8..14690b1b 100644 --- a/starpilot/system/the_pond/the_pond.py +++ b/starpilot/system/the_pond/the_pond.py @@ -4249,80 +4249,98 @@ def setup(app): @app.route("/api/themes/apply", methods=["POST"]) def apply_theme(): - form_data = request.form.to_dict(flat=True) - files = request.files + try: + form_data = request.form.to_dict(flat=True) + files = request.files - if not form_data.get("themeName"): - form_data["themeName"] = f"tmp_{secrets.token_hex(8)}" + if not form_data.get("themeName"): + form_data["themeName"] = f"tmp_{secrets.token_hex(8)}" - temp_path, error = utilities.create_theme(form_data, files, temporary=True) - if error: - return {"error": error}, 400 + temp_path, error = utilities.create_theme(form_data, files, temporary=True) + if error: + return jsonify({"error": error}), 400 - save_checklist = json.loads(form_data.get("saveChecklist", "{}")) + save_checklist = json.loads(form_data.get("saveChecklist", "{}")) - if save_checklist.get("colors"): - asset_location = temp_path / "colors" - save_location = ACTIVE_THEME_PATH / "colors" - if save_location.exists() or save_location.is_symlink(): - delete_file(save_location) - if asset_location.exists(): - save_location.parent.mkdir(parents=True, exist_ok=True) - save_location.symlink_to(asset_location, target_is_directory=True) + if save_checklist.get("colors") and temp_path is not None: + asset_location = temp_path / "colors" + save_location = ACTIVE_THEME_PATH / "colors" + if save_location.exists() or save_location.is_symlink(): + delete_file(save_location) + if asset_location.exists(): + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) - if save_checklist.get("distance_icons"): - asset_location = temp_path / "distance_icons" - save_location = ACTIVE_THEME_PATH / "distance_icons" - if save_location.exists() or save_location.is_symlink(): - delete_file(save_location) - if asset_location.exists(): - save_location.parent.mkdir(parents=True, exist_ok=True) - save_location.symlink_to(asset_location, target_is_directory=True) + if save_checklist.get("distance_icons") and temp_path is not None: + asset_location = temp_path / "distance_icons" + save_location = ACTIVE_THEME_PATH / "distance_icons" + if save_location.exists() or save_location.is_symlink(): + delete_file(save_location) + if asset_location.exists(): + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) - if save_checklist.get("icons"): - asset_location = temp_path / "icons" - save_location = ACTIVE_THEME_PATH / "icons" - if save_location.exists() or save_location.is_symlink(): - delete_file(save_location) - if asset_location.exists(): - save_location.parent.mkdir(parents=True, exist_ok=True) - save_location.symlink_to(asset_location, target_is_directory=True) + if save_checklist.get("icons") and temp_path is not None: + asset_location = temp_path / "icons" + save_location = ACTIVE_THEME_PATH / "icons" + if save_location.exists() or save_location.is_symlink(): + delete_file(save_location) + if asset_location.exists(): + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) - if save_checklist.get("sounds"): - asset_location = temp_path / "sounds" - save_location = ACTIVE_THEME_PATH / "sounds" - if save_location.exists() or save_location.is_symlink(): - delete_file(save_location) - if asset_location.exists(): - save_location.parent.mkdir(parents=True, exist_ok=True) - save_location.symlink_to(asset_location, target_is_directory=True) + if save_checklist.get("sounds") and temp_path is not None: + asset_location = temp_path / "sounds" + save_location = ACTIVE_THEME_PATH / "sounds" + if save_location.exists() or save_location.is_symlink(): + delete_file(save_location) + if asset_location.exists(): + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) - if save_checklist.get("turn_signals"): - asset_location = temp_path / "signals" - save_location = ACTIVE_THEME_PATH / "signals" - if save_location.exists() or save_location.is_symlink(): - delete_file(save_location) - if asset_location.exists(): - save_location.parent.mkdir(parents=True, exist_ok=True) - save_location.symlink_to(asset_location, target_is_directory=True) + if save_checklist.get("turn_signals") and temp_path is not None: + asset_location = temp_path / "signals" + save_location = ACTIVE_THEME_PATH / "signals" + if save_location.exists() or save_location.is_symlink(): + delete_file(save_location) + if asset_location.exists(): + save_location.parent.mkdir(parents=True, exist_ok=True) + save_location.symlink_to(asset_location, target_is_directory=True) - wheel_location = temp_path / "WheelIcon" - wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel" - if wheel_location.exists(): - if wheel_save_location.exists(): - delete_file(wheel_save_location) + wheel_location = (temp_path / "WheelIcon") if temp_path is not None else None + wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel" + if wheel_location is not None and wheel_location.exists(): + if wheel_save_location.exists(): + delete_file(wheel_save_location) - wheel_save_location.mkdir(parents=True, exist_ok=True) - for file in wheel_location.iterdir(): - destination_file = wheel_save_location / file.name - delete_file(destination_file) - destination_file.symlink_to(file) + wheel_save_location.mkdir(parents=True, exist_ok=True) + for file in wheel_location.iterdir(): + destination_file = wheel_save_location / file.name + delete_file(destination_file) + destination_file.symlink_to(file) - params.put_bool("PersonalizeOpenpilot", True) - params_memory.put_bool("UseActiveTheme", True) + params.put_bool("PersonalizeOpenpilot", True) + params_memory.put_bool("UseActiveTheme", True) - update_starpilot_toggles() - return {"message": "Theme applied successfully!"}, 200 + update_starpilot_toggles() + return jsonify({"message": "Theme applied successfully!"}), 200 + except Exception as e: + return jsonify({"error": f"Failed to apply theme: {e}"}), 500 + + def _resolve_stock_theme_asset_path(asset_path): + stock_asset_path = STOCK_THEME_PATH / asset_path + if stock_asset_path.exists(): + return stock_asset_path + + stock_fallbacks = { + "steering_wheel/wheel.png": STOCK_THEME_PATH.parents[2] / "selfdrive" / "assets" / "icons" / "chffr_wheel.png", + } + + fallback_path = stock_fallbacks.get(asset_path) + if fallback_path is not None and fallback_path.exists(): + return fallback_path + + return stock_asset_path @app.route("/api/themes/asset//") def get_theme_asset(theme, asset_path): @@ -4331,7 +4349,7 @@ def setup(app): if theme_type == "active" or theme == "__active__": file_path = ACTIVE_THEME_PATH / asset_path elif theme_type == "stock" or theme == "__stock__": - file_path = STOCK_THEME_PATH / asset_path + file_path = _resolve_stock_theme_asset_path(asset_path) elif asset_path.startswith("steering_wheels/"): file_path = THEME_SAVE_PATH / asset_path elif asset_path.startswith("steering_wheel/") and "holiday" in theme_type: @@ -4647,12 +4665,8 @@ def setup(app): steering_wheel_path = None if theme_type == "stock" or theme_path == "__stock__": - steering_dir = theme_dir / "steering_wheel" - if steering_dir.exists() and steering_dir.is_dir(): - for file in steering_dir.iterdir(): - if file.is_file() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]: - steering_wheel_path = f"steering_wheel/{file.name}" - break + if _resolve_stock_theme_asset_path("steering_wheel/wheel.png").exists(): + steering_wheel_path = "steering_wheel/wheel.png" elif "holiday" in theme_type: steering_dir = theme_dir / "steering_wheel" if steering_dir.exists() and steering_dir.is_dir():