This commit is contained in:
firestar5683
2026-03-30 01:11:20 -05:00
parent a4444ce617
commit f6b41914b9
2 changed files with 159 additions and 106 deletions
@@ -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;
+83 -69
View File
@@ -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():