#include "starpilot/ui/qt/offroad/theme_settings.h" #include "system/hardware/hw.h" bool isUserCreatedTheme(const QString &themeName) { return themeName.endsWith("-user_created"); } void updateAssetParam(const QString &assetParam, Params ¶ms, const QString &value, bool add) { QStringList assets = QString::fromStdString(params.get(assetParam.toStdString())).split(",", QString::SkipEmptyParts); if (add) { if (!assets.contains(value)) { assets.append(value); } } else { assets.removeAll(value); } assets.sort(); params.put(assetParam.toStdString(), assets.join(",").toStdString()); } void deleteThemeAsset(QDir &directory, const QString &subFolder, const QString &assetParam, const QString &themeToDelete, Params ¶ms) { bool useFiles = subFolder.isEmpty(); QString baseName = themeToDelete.toLower(); baseName.replace("(", "-").replace(")", "").replace(" ", "-"); baseName.remove(QRegularExpression("[^a-z0-9\\-]")); while (baseName.endsWith("-")) { baseName.chop(1); } QString baseUnderscore = baseName; baseUnderscore.replace("-", "_"); QStringList candidateNames = { baseName, baseName + "-user-created", baseUnderscore, baseUnderscore + "-user_created" }; if (useFiles) { QStringList files = directory.entryList(QDir::Files); for (QString &file : files) { QString normalizedFile = QFileInfo(file).baseName().toLower(); normalizedFile.replace("_", "-"); normalizedFile.remove(QRegularExpression("[^a-z0-9\\-~]")); if (candidateNames.contains(normalizedFile)) { QFile::remove(directory.filePath(file)); break; } } } else { for (QString &candidate : candidateNames) { QString fullSubPath = QDir(candidate).filePath(subFolder); QDir targetDir(directory.filePath(fullSubPath)); if (targetDir.exists()) { targetDir.removeRecursively(); break; } } } updateAssetParam(assetParam, params, themeToDelete, true); } void downloadThemeAsset(const QString &input, const std::string ¶mKey, const QString &assetParam, Params ¶ms, Params ¶ms_memory) { if (paramKey == "BootLogoToDownload") { params_memory.put(paramKey, input.trimmed().toStdString()); return; } QString output = input; output.replace(" - by: ", "~"); int tilde = output.indexOf("~"); if (tilde >= 0) { output = output.left(tilde).toLower() + "~" + output.mid(tilde + 1); } else { output = output.toLower(); } output.remove("(").remove(")"); output.replace(" ", input.contains("(") ? "-" : "_"); params_memory.put(paramKey, output.toStdString()); } QStringList getHolidayThemes() { return QStringList() << "New Year's" << "Valentine's Day" << "St. Patrick's Day" << "World Frog Day" << "April Fools" << "Easter" << "May the Fourth" << "Cinco de Mayo" << "Stitch Day" << "Fourth of July" << "Halloween" << "Thanksgiving" << "Christmas"; } QStringList getThemeList(const bool &randomThemes, const QDir &themePacksDirectory, const QString &subFolder, const QString &assetParam, Params ¶ms) { bool useFiles = subFolder.isEmpty(); QString currentAsset = randomThemes ? "" : QString::fromStdString(params.get(assetParam.toStdString())); QStringList themeList; for (const QFileInfo &entry : themePacksDirectory.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) { if (entry.baseName() == currentAsset) { continue; } if (useFiles && entry.isDir()) { continue; } if (!useFiles) { QString targetPath = QDir(entry.filePath()).filePath(subFolder); if (!QFileInfo(targetPath).exists()) { continue; } } QString baseName = entry.baseName(); bool userCreated = isUserCreatedTheme(baseName); if (userCreated) { baseName = baseName.replace("-user_created", ""); } int tildeIdx = baseName.indexOf("~"); QString creator; if (tildeIdx >= 0) { creator = baseName.mid(tildeIdx + 1); baseName = baseName.left(tildeIdx); } QStringList parts = baseName.split(baseName.contains("-") ? "-" : "_", QString::SkipEmptyParts); for (QString &part : parts) { part[0] = part[0].toUpper(); } QString displayName; if (userCreated) { displayName = parts.join(" "); } else { displayName = (parts.size() <= 1 || useFiles || !baseName.contains("-")) ? parts.join(" ") : QString("%1 (%2)").arg(parts[0], parts.mid(1).join(" ")); } if (userCreated) { displayName += " ๐ŸŒŸ"; } if (!creator.isEmpty()) { displayName += " - by: " + creator; } themeList.append(displayName); } return themeList; } QString getThemeName(const std::string ¶mKey, Params ¶ms) { QString value = QString::fromStdString(params.get(paramKey)); QString baseName = value; int tildeIdx = baseName.indexOf("~"); QString creator; if (tildeIdx >= 0) { creator = baseName.mid(tildeIdx + 1); baseName = baseName.left(tildeIdx); } QStringList parts = baseName.split(baseName.contains("-") ? "-" : "_", QString::SkipEmptyParts); for (QString &part : parts) { part[0] = part[0].toUpper(); } QString displayName; if (baseName.contains("-") && parts.size() > 1) { displayName = QString("%1 (%2)").arg(parts[0], parts.mid(1).join(" ")); } else { displayName = parts.join(" "); } if (isUserCreatedTheme(value)) { displayName = displayName.split(" (")[0] + " ๐ŸŒŸ"; } if (!creator.isEmpty()) { displayName += " - by: " + creator; } return displayName; } QString storeThemeName(const QString &input, const std::string ¶mKey, Params ¶ms) { QString output = input.toLower().remove("(").remove(")").remove("'").remove("."); output.replace(" ", input.contains("(") ? "-" : "_"); output.replace("_๐ŸŒŸ", "-user_created"); output = output.trimmed(); params.put(paramKey, output.toStdString()); return getThemeName(paramKey, params); } StarPilotThemesPanel::StarPilotThemesPanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) { forceOpenDescriptions = forceOpen; if (Hardware::PC()) { const QString themesRoot = QString::fromStdString(Path::comma_home() + "/starpilot/data/themes/"); bootLogosDirectory.setPath(themesRoot + "bootlogos/"); themePacksDirectory.setPath(themesRoot + "theme_packs/"); wheelsDirectory.setPath(themesRoot + "steering_wheels/"); } QDir().mkpath(bootLogosDirectory.path()); QDir().mkpath(themePacksDirectory.path()); QDir().mkpath(wheelsDirectory.path()); QStackedLayout *themesLayout = new QStackedLayout(); addItem(themesLayout); StarPilotListWidget *themesList = new StarPilotListWidget(this); ScrollView *themesPanel = new ScrollView(themesList, this); themesLayout->addWidget(themesPanel); StarPilotListWidget *customThemesList = new StarPilotListWidget(this); ScrollView *customThemesPanel = new ScrollView(customThemesList, this); themesLayout->addWidget(customThemesPanel); const std::vector> themeToggles { {"CustomThemes", tr("Custom Themes"), tr("The overall look and feel of openpilot. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), "../../starpilot/assets/toggle_icons/icon_frog.png"}, {"BootLogo", tr("Boot Logo"), tr("The boot logo shown while the device starts."), ""}, {"ColorScheme", tr("Color Scheme"), tr("The color scheme used throughout openpilot. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"DistanceIconPack", tr("Distance Button"), tr("The distance button icons shown on the driving screen. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"IconPack", tr("Icon Pack"), tr("The icon style used across openpilot. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"SignalAnimation", tr("Turn Signal"), tr("Themed turn-signal animations. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"SoundPack", tr("Sound Pack"), tr("The sound pack used by openpilot. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"WheelIcon", tr("Steering Wheel"), tr("The steering-wheel icon shown at the top-right of the driving screen. Use the \"Theme Maker\" in \"The Galaxy\" to create and share your own themes!"), ""}, {"DownloadStatusLabel", tr("Download Status"), "", ""}, {"HolidayThemes", tr("Holiday Themes"), tr("Themes based on U.S. holidays. Minor holidays last one day; major holidays (Christmas, Easter, Halloween) run for a full week."), "../../starpilot/assets/toggle_icons/icon_calendar.png"}, {"RainbowPath", tr("Rainbow Path"), tr("Color the driving path like a Mario Kartโ€“style \"Rainbow Road\"."), "../../starpilot/assets/toggle_icons/icon_rainbow.png"}, {"RandomEvents", tr("Random Events"), tr("Occasional on-screen effects triggered by driving conditions. These are purely a visual and don't impact how openpilot drives!"), "../../starpilot/assets/toggle_icons/icon_random.png"}, {"RandomThemes", tr("Random Themes"), tr("Pick a random theme between each drive from the themes you have downloaded. Great for variety without changing settings while driving."), "../../starpilot/assets/toggle_icons/icon_random_themes.png"}, {"StartupAlert", tr("Startup Alert"), tr("Customize the \"Startup Alert\" message shown at the start of each drive."), "../../starpilot/assets/toggle_icons/icon_message.png"} }; for (const auto &[param, title, desc, icon] : themeToggles) { AbstractControl *themeToggle; if (param == "CustomThemes") { StarPilotManageControl *personalizeOpenpilotToggle = new StarPilotManageControl(param, title, desc, icon); QObject::connect(personalizeOpenpilotToggle, &StarPilotManageControl::manageButtonClicked, [customThemesPanel, themesLayout]() { themesLayout->setCurrentWidget(customThemesPanel); }); themeToggle = personalizeOpenpilotToggle; } else if (param == "BootLogo") { manageBootLogosButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageBootLogosButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { // Show all downloaded boot logos, including the currently selected one. QStringList bootLogos = getThemeList(true, QDir(bootLogosDirectory.path()), "", "BootLogo", params); if (id == 0) { QString bootLogoToDelete = MultiOptionDialog::getSelection(tr("Select a boot logo to delete"), bootLogos, "", this); if (!bootLogoToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" boot logo?").arg(bootLogoToDelete), tr("Delete"), this)) { bootLogosDownloaded = false; deleteThemeAsset(bootLogosDirectory, "", "DownloadableBootLogos", bootLogoToDelete, params); } } else if (id == 1) { if (bootLogoDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { bootLogoDownloading = false; cancellingDownload = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableBootLogos = QString::fromStdString(params.get("DownloadableBootLogos")).split(","); bootLogoToDownload = MultiOptionDialog::getSelection(tr("Select a boot logo to download"), downloadableBootLogos, "", this); if (!bootLogoToDownload.isEmpty()) { manageBootLogosButton->setValue(storeThemeName(bootLogoToDownload, "BootLogo", params)); bootLogoDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(bootLogoToDownload, "BootLogoToDownload", "DownloadableBootLogos", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { QString bootLogoToSelect = MultiOptionDialog::getSelection(tr("Select a boot logo"), bootLogos, getThemeName("BootLogo", params), this); if (!bootLogoToSelect.isEmpty()) { manageBootLogosButton->setValue(storeThemeName(bootLogoToSelect, "BootLogo", params)); } } }); manageBootLogosButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageBootLogosButton; } else if (param == "ColorScheme") { manageColorSchemeButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageColorSchemeButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList colorSchemes = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "colors", "ColorScheme", params); if (id == 0) { QString colorSchemeToDelete = MultiOptionDialog::getSelection(tr("Select a color scheme to delete"), colorSchemes, "", this); if (!colorSchemeToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" color scheme?").arg(colorSchemeToDelete), tr("Delete"), this)) { colorsDownloaded = false; deleteThemeAsset(themePacksDirectory, "colors", "DownloadableColors", colorSchemeToDelete, params); } } else if (id == 1) { if (colorDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; colorDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableColorSchemes = QString::fromStdString(params.get("DownloadableColors")).split(","); colorSchemeToDownload = MultiOptionDialog::getSelection(tr("Select a color scheme to download"), downloadableColorSchemes, "", this); if (!colorSchemeToDownload.isEmpty()) { colorDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(colorSchemeToDownload, "ColorToDownload", "DownloadableColors", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { colorSchemes.append("Stock"); colorSchemes.append(getHolidayThemes()); colorSchemes.sort(); QString colorSchemeToSelect = MultiOptionDialog::getSelection(tr("Select a color scheme"), colorSchemes, getThemeName("ColorScheme", params), this); if (!colorSchemeToSelect.isEmpty()) { manageColorSchemeButton->setValue(storeThemeName(colorSchemeToSelect, "ColorScheme", params)); } } }); manageColorSchemeButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageColorSchemeButton; } else if (param == "DistanceIconPack") { manageDistanceIconPackButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageDistanceIconPackButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList distanceIconPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "distance_icons", "DistanceIconPack", params); if (id == 0) { QString distanceIconPackToDelete = MultiOptionDialog::getSelection(tr("Select a distance icon pack to delete"), distanceIconPacks, "", this); if (!distanceIconPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" distance icon pack?").arg(distanceIconPackToDelete), tr("Delete"), this)) { distanceIconsDownloaded = false; deleteThemeAsset(themePacksDirectory, "distance_icons", "DownloadableDistanceIcons", distanceIconPackToDelete, params); } } else if (id == 1) { if (distanceIconDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; distanceIconDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableDistanceIconPacks = QString::fromStdString(params.get("DownloadableDistanceIcons")).split(","); distanceIconPackToDownload = MultiOptionDialog::getSelection(tr("Select a distance icon pack to download"), downloadableDistanceIconPacks, "", this); if (!distanceIconPackToDownload.isEmpty()) { distanceIconDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(distanceIconPackToDownload, "DistanceIconToDownload", "DownloadableDistanceIcons", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { distanceIconPacks.append("Stock"); distanceIconPacks.append(getHolidayThemes()); distanceIconPacks.sort(); QString distanceIconPackToSelect = MultiOptionDialog::getSelection(tr("Select a distance icon pack"), distanceIconPacks, getThemeName("DistanceIconPack", params), this); if (!distanceIconPackToSelect.isEmpty()) { manageDistanceIconPackButton->setValue(storeThemeName(distanceIconPackToSelect, "DistanceIconPack", params)); } } }); manageDistanceIconPackButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageDistanceIconPackButton; } else if (param == "IconPack") { manageIconPackButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageIconPackButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList iconPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "icons", "IconPack", params); if (id == 0) { QString iconPackToDelete = MultiOptionDialog::getSelection(tr("Select an icon pack to delete"), iconPacks, "", this); if (!iconPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" icon pack?").arg(iconPackToDelete), tr("Delete"), this)) { iconsDownloaded = false; deleteThemeAsset(themePacksDirectory, "icons", "DownloadableIcons", iconPackToDelete, params); } } else if (id == 1) { if (iconDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; iconDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableIconPacks = QString::fromStdString(params.get("DownloadableIcons")).split(","); iconPackToDownload = MultiOptionDialog::getSelection(tr("Select an icon pack to download"), downloadableIconPacks, "", this); if (!iconPackToDownload.isEmpty()) { iconDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(iconPackToDownload, "IconToDownload", "DownloadableIcons", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { iconPacks.append("Stock"); iconPacks.append(getHolidayThemes()); iconPacks.sort(); QString iconPackToSelect = MultiOptionDialog::getSelection(tr("Select an icon pack"), iconPacks, getThemeName("IconPack", params), this); if (!iconPackToSelect.isEmpty()) { manageIconPackButton->setValue(storeThemeName(iconPackToSelect, "IconPack", params)); } } }); manageIconPackButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageIconPackButton; } else if (param == "SignalAnimation") { manageSignalAnimationButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageSignalAnimationButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList signalAnimations = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "signals", "SignalAnimation", params); if (id == 0) { QString signalAnimationToDelete = MultiOptionDialog::getSelection(tr("Select a signal animation to delete"), signalAnimations, "", this); if (!signalAnimationToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" signal animation?").arg(signalAnimationToDelete), tr("Delete"), this)) { signalsDownloaded = false; deleteThemeAsset(themePacksDirectory, "signals", "DownloadableSignals", signalAnimationToDelete, params); } } else if (id == 1) { if (signalDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; signalDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableSignalAnimations = QString::fromStdString(params.get("DownloadableSignals")).split(","); signalAnimationToDownload = MultiOptionDialog::getSelection(tr("Select a signal animation to download"), downloadableSignalAnimations, "", this); if (!signalAnimationToDownload.isEmpty()) { signalDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(signalAnimationToDownload, "SignalToDownload", "DownloadableSignals", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { signalAnimations.append("None"); signalAnimations.append(getHolidayThemes()); signalAnimations.sort(); QString signalAnimationToSelect = MultiOptionDialog::getSelection(tr("Select a signal animation"), signalAnimations, getThemeName("SignalAnimation", params), this); if (!signalAnimationToSelect.isEmpty()) { manageSignalAnimationButton->setValue(storeThemeName(signalAnimationToSelect, "SignalAnimation", params)); } } }); manageSignalAnimationButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageSignalAnimationButton; } else if (param == "SoundPack") { manageSoundPackButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageSoundPackButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList soundPacks = getThemeList(randomThemes, QDir(themePacksDirectory.path()), "sounds", "SoundPack", params); if (id == 0) { QString soundPackToDelete = MultiOptionDialog::getSelection(tr("Select a sound pack to delete"), soundPacks, "", this); if (!soundPackToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" sound pack?").arg(soundPackToDelete), tr("Delete"), this)) { soundsDownloaded = false; deleteThemeAsset(themePacksDirectory, "sounds", "DownloadableSounds", soundPackToDelete, params); } } else if (id == 1) { if (soundDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; soundDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableSoundPacks = QString::fromStdString(params.get("DownloadableSounds")).split(","); soundPackToDownload = MultiOptionDialog::getSelection(tr("Select a sound pack to download"), downloadableSoundPacks, "", this); if (!soundPackToDownload.isEmpty()) { soundDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(soundPackToDownload, "SoundToDownload", "DownloadableSounds", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { soundPacks.append("Stock"); soundPacks.append(getHolidayThemes()); soundPacks.sort(); QString soundPackToSelect = MultiOptionDialog::getSelection(tr("Select a sound pack"), soundPacks, getThemeName("SoundPack", params), this); if (!soundPackToSelect.isEmpty()) { manageSoundPackButton->setValue(storeThemeName(soundPackToSelect, "SoundPack", params)); } } }); manageSoundPackButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageSoundPackButton; } else if (param == "WheelIcon") { manageWheelIconsButton = new StarPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); QObject::connect(manageWheelIconsButton, &StarPilotButtonsControl::buttonClicked, [this](int id) { QStringList wheelIcons = getThemeList(randomThemes, QDir(wheelsDirectory.path()), "", "WheelIcon", params); if (id == 0) { QString wheelIconToDelete = MultiOptionDialog::getSelection(tr("Select a steering wheel to delete"), wheelIcons, "", this); if (!wheelIconToDelete.isEmpty() && ConfirmationDialog::confirm(tr("Delete the \"%1\" steering wheel?").arg(wheelIconToDelete), tr("Delete"), this)) { wheelsDownloaded = false; deleteThemeAsset(wheelsDirectory, "", "DownloadableWheels", wheelIconToDelete, params); } } else if (id == 1) { if (wheelDownloading) { cancellingDownload = true; params_memory.putBool("CancelThemeDownload", true); QTimer::singleShot(2500, [this]() { cancellingDownload = false; wheelDownloading = false; themeDownloading = false; params_memory.putBool("CancelThemeDownload", false); }); } else { QStringList downloadableWheels = QString::fromStdString(params.get("DownloadableWheels")).split(","); wheelToDownload = MultiOptionDialog::getSelection(tr("Select a steering wheel to download"), downloadableWheels, "", this); if (!wheelToDownload.isEmpty()) { wheelDownloading = true; themeDownloading = true; params_memory.put("ThemeDownloadProgress", "Downloading..."); downloadThemeAsset(wheelToDownload, "WheelToDownload", "DownloadableWheels", params, params_memory); downloadStatusLabel->setText(tr("Downloading...")); } } } else if (id == 2) { wheelIcons.append("None"); wheelIcons.append("Stock"); wheelIcons.append(getHolidayThemes()); wheelIcons.sort(); QString steeringWheelToSelect = MultiOptionDialog::getSelection(tr("Select a steering wheel"), wheelIcons, getThemeName("WheelIcon", params), this); if (!steeringWheelToSelect.isEmpty()) { manageWheelIconsButton->setValue(storeThemeName(steeringWheelToSelect, "WheelIcon", params)); } } }); manageWheelIconsButton->setValue(getThemeName(param.toStdString(), params)); themeToggle = manageWheelIconsButton; } else if (param == "DownloadStatusLabel") { downloadStatusLabel = new LabelControl(title, tr("Idle")); themeToggle = downloadStatusLabel; } else if (param == "RandomThemes") { std::vector randomThemesToggles{"RandomThemesHolidays"}; std::vector randomThemesToggleNames{tr("Include Holiday Themes")}; themeToggle = new StarPilotButtonToggleControl(param, title, desc, icon, randomThemesToggles, randomThemesToggleNames); } else if (param == "StartupAlert") { StarPilotButtonsControl *startupAlertButton = new StarPilotButtonsControl(title, desc, icon, {tr("STOCK"), tr("STARPILOT"), tr("CUSTOM"), tr("CLEAR")}, true); QString currentTop = QString::fromStdString(params.get("StartupMessageTop")); QString currentBottom = QString::fromStdString(params.get("StartupMessageBottom")); QString stockTop = "Be ready to take over at any time"; QString stockBottom = "Always keep hands on wheel and eyes on road"; QString starpilotTop = "Hop in and buckle up!"; QString starpilotBottom = "Human-tested, frog-approved ๐Ÿธ"; if (currentTop == stockTop && currentBottom == stockBottom) { startupAlertButton->setCheckedButton(0); } else if (currentTop == starpilotTop && currentBottom == starpilotBottom) { startupAlertButton->setCheckedButton(1); } else if (!currentTop.isEmpty() || !currentBottom.isEmpty()) { startupAlertButton->setCheckedButton(2); } QObject::connect(startupAlertButton, &StarPilotButtonsControl::buttonClicked, [=](int id) { int maxLengthTop = 35; int maxLengthBottom = 45; if (id == 0) { params.put("StartupMessageTop", stockTop.toStdString()); params.put("StartupMessageBottom", stockBottom.toStdString()); } else if (id == 1) { params.put("StartupMessageTop", starpilotTop.toStdString()); params.put("StartupMessageBottom", starpilotBottom.toStdString()); } else if (id == 2) { QString currentTop = QString::fromStdString(params.get("StartupMessageTop")); QString newTop = InputDialog::getText(tr("Enter the text for the top half"), this, tr("Characters: 0/%1").arg(maxLengthTop), false, 1, currentTop, maxLengthTop).trimmed(); if (!newTop.isEmpty()) { params.put("StartupMessageTop", newTop.toStdString()); QString currentBottom = QString::fromStdString(params.get("StartupMessageBottom")); QString newBottom = InputDialog::getText(tr("Enter the text for the bottom half"), this, tr("Characters: 0/%1").arg(maxLengthBottom), false, 1, currentBottom, maxLengthBottom).trimmed(); if (!newBottom.isEmpty()) { params.put("StartupMessageBottom", newBottom.toStdString()); } } } else if (id == 3) { if (StarPilotConfirmationDialog::yesorno(tr("Are you sure you want to completely reset your startup message?"), this)) { params.remove("StartupMessageTop"); params.remove("StartupMessageBottom"); startupAlertButton->clearCheckedButtons(); } } }); themeToggle = startupAlertButton; } else { themeToggle = new ParamControl(param, title, desc, icon); } toggles[param] = themeToggle; if (customThemeKeys.contains(param)) { customThemesList->addItem(themeToggle); } else { themesList->addItem(themeToggle); if (param == "CustomThemes") { parentKeys.insert(param); } } if (StarPilotManageControl *frogPilotManageToggle = qobject_cast(themeToggle)) { QObject::connect(frogPilotManageToggle, &StarPilotManageControl::manageButtonClicked, [this]() { emit openSubPanel(); openDescriptions(forceOpenDescriptions, toggles); }); } QObject::connect(themeToggle, &AbstractControl::hideDescriptionEvent, [this]() { update(); }); QObject::connect(themeToggle, &AbstractControl::showDescriptionEvent, [this]() { update(); }); } openDescriptions(forceOpenDescriptions, toggles); QObject::connect(static_cast(toggles["CustomThemes"]), &ToggleControl::toggleFlipped, this, &StarPilotThemesPanel::updateToggles); QObject::connect(static_cast(toggles["RandomThemes"]), &ToggleControl::toggleFlipped, [this](bool state) { if (state) { ConfirmationDialog::alert(tr("\"Random Themes\" only works with downloaded themes, so make sure you download the themes you want it to use!"), this); manageColorSchemeButton->setValue(""); manageColorSchemeButton->setVisibleButton(2, false); manageDistanceIconPackButton->setValue(""); manageDistanceIconPackButton->setVisibleButton(2, false); manageIconPackButton->setValue(""); manageIconPackButton->setVisibleButton(2, false); manageSignalAnimationButton->setValue(""); manageSignalAnimationButton->setVisibleButton(2, false); manageSoundPackButton->setValue(""); manageSoundPackButton->setVisibleButton(2, false); manageWheelIconsButton->setValue(""); manageWheelIconsButton->setVisibleButton(2, false); } else { manageColorSchemeButton->setValue(getThemeName("ColorScheme", params)); manageColorSchemeButton->setVisibleButton(2, true); manageDistanceIconPackButton->setValue(getThemeName("DistanceIconPack", params)); manageDistanceIconPackButton->setVisibleButton(2, true); manageIconPackButton->setValue(getThemeName("IconPack", params)); manageIconPackButton->setVisibleButton(2, true); manageSignalAnimationButton->setValue(getThemeName("SignalAnimation", params)); manageSignalAnimationButton->setVisibleButton(2, true); manageSoundPackButton->setValue(getThemeName("SoundPack", params)); manageSoundPackButton->setVisibleButton(2, true); manageWheelIconsButton->setValue(getThemeName("WheelIcon", params)); manageWheelIconsButton->setVisibleButton(2, true); } randomThemes = state; }); QObject::connect(parent, &StarPilotSettingsWindow::closeSubPanel, [themesLayout, themesPanel, this] { openDescriptions(forceOpenDescriptions, toggles); themesLayout->setCurrentWidget(themesPanel); }); QObject::connect(uiState(), &UIState::uiUpdate, this, &StarPilotThemesPanel::updateState); } void StarPilotThemesPanel::showEvent(QShowEvent *event) { bootLogosDownloaded = params.get("DownloadableBootLogos").empty(); colorsDownloaded = params.get("DownloadableColors").empty(); distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty(); iconsDownloaded = params.get("DownloadableIcons").empty(); signalsDownloaded = params.get("DownloadableSignals").empty(); soundsDownloaded = params.get("DownloadableSounds").empty(); wheelsDownloaded = params.get("DownloadableWheels").empty(); if (params.getBool("RandomThemes")) { manageColorSchemeButton->setValue(""); manageColorSchemeButton->setVisibleButton(2, false); manageDistanceIconPackButton->setValue(""); manageDistanceIconPackButton->setVisibleButton(2, false); manageIconPackButton->setValue(""); manageIconPackButton->setVisibleButton(2, false); manageSignalAnimationButton->setValue(""); manageSignalAnimationButton->setVisibleButton(2, false); manageSoundPackButton->setValue(""); manageSoundPackButton->setVisibleButton(2, false); manageWheelIconsButton->setValue(""); manageWheelIconsButton->setVisibleButton(2, false); randomThemes = true; } updateToggles(); } void StarPilotThemesPanel::updateState(const UIState &s, const StarPilotUIState &fs) { if (!isVisible() || finalizingDownload) { return; } const StarPilotUIScene &starpilot_scene = fs.starpilot_scene; if (themeDownloading) { QString progress = QString::fromStdString(params_memory.get("ThemeDownloadProgress")); bool downloadFailed = progress.contains(QRegularExpression("cancelled|exists|failed|offline", QRegularExpression::CaseInsensitiveOption)); if (progress != "Downloading...") { static const QMap progressTranslations = { {"Download cancelled...", tr("Download cancelled...")}, {"Download failed...", tr("Download failed...")}, {"Downloaded!", tr("Downloaded!")}, {"GitHub and GitLab are offline...", tr("GitHub and GitLab are offline...")}, {"Repository unavailable", tr("Repository unavailable")}, {"Unpacking theme...", tr("Unpacking theme...")}, {"Verifying authenticity...", tr("Verifying authenticity...")} }; if (progressTranslations.contains(progress)) { downloadStatusLabel->setText(progressTranslations[progress]); } else if (progress.endsWith("%")) { downloadStatusLabel->setText(progress); } else { downloadStatusLabel->setText(tr("Idle")); } } if (progress == "Downloaded!" || downloadFailed) { finalizingDownload = true; QTimer::singleShot(2500, [this]() { bootLogoDownloading = false; colorDownloading = false; distanceIconDownloading = false; finalizingDownload = false; iconDownloading = false; signalDownloading = false; soundDownloading = false; themeDownloading = false; wheelDownloading = false; bootLogosDownloaded = params.get("DownloadableBootLogos").empty(); colorsDownloaded = params.get("DownloadableColors").empty(); distanceIconsDownloaded = params.get("DownloadableDistanceIcons").empty(); iconsDownloaded = params.get("DownloadableIcons").empty(); signalsDownloaded = params.get("DownloadableSignals").empty(); soundsDownloaded = params.get("DownloadableSounds").empty(); wheelsDownloaded = params.get("DownloadableWheels").empty(); params_memory.remove("CancelThemeDownload"); params_memory.remove("ThemeDownloadProgress"); downloadStatusLabel->setText(tr("Idle")); }); } } bool parked = !s.scene.started || starpilot_scene.parked || parent->isFrogsGoMoo; manageBootLogosButton->setText(1, bootLogoDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageBootLogosButton->setEnabledButtons(0, !themeDownloading); manageBootLogosButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || bootLogoDownloading) && !cancellingDownload && !finalizingDownload && !bootLogosDownloaded && parked); manageBootLogosButton->setEnabledButtons(2, !themeDownloading); manageColorSchemeButton->setText(1, colorDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageColorSchemeButton->setEnabledButtons(0, !themeDownloading); manageColorSchemeButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || colorDownloading) && !cancellingDownload && !finalizingDownload && !colorsDownloaded && parked); manageColorSchemeButton->setEnabledButtons(2, !themeDownloading); manageDistanceIconPackButton->setText(1, distanceIconDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageDistanceIconPackButton->setEnabledButtons(0, !themeDownloading); manageDistanceIconPackButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || distanceIconDownloading) && !cancellingDownload && !finalizingDownload && !distanceIconsDownloaded && parked); manageDistanceIconPackButton->setEnabledButtons(2, !themeDownloading); manageIconPackButton->setText(1, iconDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageIconPackButton->setEnabledButtons(0, !themeDownloading); manageIconPackButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || iconDownloading) && !cancellingDownload && !finalizingDownload && !iconsDownloaded && parked); manageIconPackButton->setEnabledButtons(2, !themeDownloading); manageSignalAnimationButton->setText(1, signalDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageSignalAnimationButton->setEnabledButtons(0, !themeDownloading); manageSignalAnimationButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || signalDownloading) && !cancellingDownload && !finalizingDownload && !signalsDownloaded && parked); manageSignalAnimationButton->setEnabledButtons(2, !themeDownloading); manageSoundPackButton->setText(1, soundDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageSoundPackButton->setEnabledButtons(0, !themeDownloading); manageSoundPackButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || soundDownloading) && !cancellingDownload && !finalizingDownload && !soundsDownloaded && parked); manageSoundPackButton->setEnabledButtons(2, !themeDownloading); manageWheelIconsButton->setText(1, wheelDownloading ? tr("CANCEL") : tr("DOWNLOAD")); manageWheelIconsButton->setEnabledButtons(0, !themeDownloading); manageWheelIconsButton->setEnabledButtons(1, starpilot_scene.online && (!themeDownloading || wheelDownloading) && !cancellingDownload && !finalizingDownload && !wheelsDownloaded && parked); manageWheelIconsButton->setEnabledButtons(2, !themeDownloading); parent->keepScreenOn = themeDownloading; } void StarPilotThemesPanel::updateToggles() { const bool showAllToggles = parent->showAllTogglesEnabled(); for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { toggle->setVisible(showAllToggles); } } for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { continue; } bool setVisible = showAllToggles || parent->tuningLevel >= parent->starpilotToggleLevels[key].toDouble(); if (!showAllToggles) { if (key == "DistanceIconPack") { setVisible &= params.getBool("QOLVisuals") && params.getBool("OnroadDistanceButton"); } else if (key == "RandomThemes") { setVisible &= params.getBool("CustomThemes"); } } toggle->setVisible(setVisible); if (setVisible) { if (customThemeKeys.contains(key)) { toggles["CustomThemes"]->setVisible(true); } } } openDescriptions(forceOpenDescriptions, toggles); update(); }