fix
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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/<path:theme>/<path:asset_path>")
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user