diff --git a/common/api.py b/common/api.py index 22fb7f65..c9e89a16 100644 --- a/common/api.py +++ b/common/api.py @@ -7,7 +7,7 @@ from openpilot.system.version import get_version from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server -API_HOST = os.getenv('API_HOST', f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}') +API_HOST = os.getenv('API_HOST', f"https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}") # name: jwt signature algorithm KEYS = {"id_rsa": "RS256", diff --git a/frogpilot/assets/theme_manager.py b/frogpilot/assets/theme_manager.py index 52a5ebb7..8f18e463 100644 --- a/frogpilot/assets/theme_manager.py +++ b/frogpilot/assets/theme_manager.py @@ -270,13 +270,8 @@ class ThemeManager: if "~" in base: base, creator = base.split("~", 1) - parts = base.replace("_", "-").split("-") - capitalized_parts = [part.capitalize() for part in parts if part] - - if len(capitalized_parts) > 1 and component != "steering_wheels": - display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})" - else: - display = " ".join(capitalized_parts) + parts = base.replace("_", " ").replace("-", " ").split() + display = " ".join(part.capitalize() for part in parts) if creator: return f"{display} - by: {creator}" diff --git a/frogpilot/common/frogpilot_backups.py b/frogpilot/common/frogpilot_backups.py index 8bd82bc9..e505cdc5 100644 --- a/frogpilot/common/frogpilot_backups.py +++ b/frogpilot/common/frogpilot_backups.py @@ -54,7 +54,7 @@ def backup_toggles(params, boot_run=False): print("Toggles are identical to the previous backup. Aborting...") return - destination = TOGGLE_BACKUPS / f"{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_auto" + destination = TOGGLE_BACKUPS / f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_auto" create_backup(Path(params_backup.get_param_path()), destination, "Successfully backed up toggles!", "Failed to backup toggles...", params) diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index f02ecca5..8c4ed850 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -277,7 +277,7 @@ class FrogPilotVariables: for source in (theme_colors, self.stock_colors): color = source.get(key) if isinstance(color, dict): - return f"#{color.get("alpha", 255):02X}{color.get("red", 255):02X}{color.get("green", 255):02X}{color.get("blue", 255):02X}" + return f"#{color.get('alpha', 255):02X}{color.get('red', 255):02X}{color.get('green', 255):02X}{color.get('blue', 255):02X}" return "#FFFFFFFF" def get_value(self, key, cast=bool, condition=True, conversion=None, default=None, min=None, max=None): @@ -739,4 +739,5 @@ class FrogPilotVariables: toggle.volt_sng = self.get_value("VoltSNG", condition=toggle.car_model == "CHEVROLET_VOLT") + process_frogpilot_toggles.cache_clear() self.params_memory.remove("FrogPilotTogglesUpdated") diff --git a/frogpilot/controls/frogpilot_planner.py b/frogpilot/controls/frogpilot_planner.py index defac891..2753e082 100644 --- a/frogpilot/controls/frogpilot_planner.py +++ b/frogpilot/controls/frogpilot_planner.py @@ -74,7 +74,7 @@ class FrogPilotPlanner: self.driving_in_curve = abs(self.lateral_acceleration) >= MINIMUM_LATERAL_ACCELERATION - self.frogpilot_events.update(v_cruise, sm, frogpilot_toggles) + self.frogpilot_events.update(long_control_active, v_cruise, sm, frogpilot_toggles) self.frogpilot_following.update(long_control_active, v_ego, sm, frogpilot_toggles) diff --git a/frogpilot/controls/lib/frogpilot_events.py b/frogpilot/controls/lib/frogpilot_events.py index deab7059..2b5f3599 100644 --- a/frogpilot/controls/lib/frogpilot_events.py +++ b/frogpilot/controls/lib/frogpilot_events.py @@ -36,7 +36,7 @@ class FrogPilotEvents: self.error_log = error_log - def update(self, v_cruise, sm, frogpilot_toggles): + def update(self, long_control_active, v_cruise, sm, frogpilot_toggles): current_alert = sm["selfdriveState"].alertType current_frogpilot_alert = sm["selfdriveState"].alertType @@ -44,9 +44,9 @@ class FrogPilotEvents: self.events.clear() - acceleration = sm["carState"].aEgo + acceleration = sm["carControl"].actuators.accel - if not sm["carState"].gasPressed: + if long_control_active: self.max_acceleration = max(acceleration, self.max_acceleration) else: self.max_acceleration = 0 diff --git a/frogpilot/system/frogpilot_tracking.py b/frogpilot/system/frogpilot_tracking.py index 42e87051..5ba913e3 100644 --- a/frogpilot/system/frogpilot_tracking.py +++ b/frogpilot/system/frogpilot_tracking.py @@ -92,7 +92,7 @@ class FrogPilotTracking: self.previous_sound = sm["frogpilotSelfdriveState"].alertSound - self.frogpilot_stats["HighestAcceleration"] = max(self.frogpilot_events.max_acceleration, self.frogpilot_stats.get("HighestAcceleration", 0)) + self.frogpilot_stats["MaxAcceleration"] = max(self.frogpilot_events.max_acceleration, self.frogpilot_stats.get("MaxAcceleration", 0)) if sm["carControl"].latActive: self.frogpilot_stats["LateralTime"] = self.frogpilot_stats.get("LateralTime", 0) + DT_MDL diff --git a/frogpilot/ui/qt/offroad/data_settings.cc b/frogpilot/ui/qt/offroad/data_settings.cc index 8bcfd89b..b4939096 100644 --- a/frogpilot/ui/qt/offroad/data_settings.cc +++ b/frogpilot/ui/qt/offroad/data_settings.cc @@ -657,10 +657,10 @@ void FrogPilotDataPanel::updateStatsLabels(FrogPilotListWidget *labelsList) { {"FrogPilotSeconds", {tr("Total Driving Time"), "time"}}, {"FrogSqueaks", {tr("Total Frog Squeaks"), "count"}}, {"GoatScreams", {tr("Total Goat Screams"), "count"}}, - {"HighestAcceleration", {tr("Highest Acceleration Rate"), "accel"}}, {"LateralTime", {tr("Time Using Lateral Control"), "timePercent"}}, {"LongestDistanceWithoutOverride", {tr("Longest Distance Without an Override"), "distance"}}, {"LongitudinalTime", {tr("Time Using Longitudinal Control"), "timePercent"}}, + {"MaxAcceleration", {tr("Highest Acceleration Rate"), "accel"}}, {"ModelTimes", {tr("Driving Models:"), "parent"}}, {"Month", {tr("Month"), "other"}}, {"NightTime", {tr("Time Driving (Nighttime)"), "timePercent"}}, diff --git a/frogpilot/ui/qt/offroad/device_settings.cc b/frogpilot/ui/qt/offroad/device_settings.cc index f390a708..42d03dbc 100644 --- a/frogpilot/ui/qt/offroad/device_settings.cc +++ b/frogpilot/ui/qt/offroad/device_settings.cc @@ -41,13 +41,7 @@ FrogPilotDevicePanel::FrogPilotDevicePanel(FrogPilotSettingsWindow *parent, bool {"ScreenRecorder", tr("Screen Recorder"), tr("Add a button to the driving screen to record the display."), ""}, {"ScreenTimeout", tr("Screen Timeout (Offroad)"), tr("How long the screen stays on after being tapped while not driving."), ""}, {"ScreenTimeoutOnroad", tr("Screen Timeout (Onroad)"), tr("How long the screen stays on after being tapped while driving."), ""}, - {"StandbyMode", tr("Standby Mode"), tr("Turn the screen off while driving and automatically wake it up for alerts or engagement state changes."), ""}, - - {"IgnoreMe", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, - {"IgnoreMe2", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, - {"IgnoreMe3", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, - {"IgnoreMe4", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, - {"IgnoreMe5", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""} + {"StandbyMode", tr("Standby Mode"), tr("Turn the screen off while driving and automatically wake it up for alerts or engagement state changes."), ""} }; for (const auto &[param, title, desc, icon] : deviceToggles) { diff --git a/frogpilot/ui/qt/offroad/lateral_settings.cc b/frogpilot/ui/qt/offroad/lateral_settings.cc index 2430e46f..021f4ddb 100644 --- a/frogpilot/ui/qt/offroad/lateral_settings.cc +++ b/frogpilot/ui/qt/offroad/lateral_settings.cc @@ -58,10 +58,7 @@ FrogPilotLateralPanel::FrogPilotLateralPanel(FrogPilotSettingsWindow *parent, bo {"NNFFLite", tr("Neural Network Feedforward (NNFF) Lite"), tr("A lightweight version of Twilsonco's \"Neural Network FeedForward\" controller. Uses the \"look-ahead\" planned lateral jerk logic from the full model to help smoothen steering adjustments in curves, but does not use the full neural network for torque calculation."), ""}, {"QOLLateral", tr("Quality of Life"), tr("Steering control changes to fine-tune how openpilot drives."), "../../frogpilot/assets/toggle_icons/icon_quality_of_life.png"}, - {"PauseLateralSpeed", tr("Pause Steering Below"), tr("Pause steering below the set speed."), ""}, - - {"IgnoreMe", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""}, - {"IgnoreMe2", "Ignore Me", "This is simply used to fix the layout when the user opens the descriptions and the menu gets wonky. No idea why it happens, but I can't be asked to properly fix it so whatever. Sue me.", ""} + {"PauseLateralSpeed", tr("Pause Steering Below"), tr("Pause steering below the set speed."), ""} }; for (const auto &[param, title, desc, icon] : lateralToggles) { diff --git a/frogpilot/ui/qt/offroad/theme_settings.cc b/frogpilot/ui/qt/offroad/theme_settings.cc index 5a9d90a9..250bac1e 100644 --- a/frogpilot/ui/qt/offroad/theme_settings.cc +++ b/frogpilot/ui/qt/offroad/theme_settings.cc @@ -1,869 +1,870 @@ -#include "frogpilot/ui/qt/offroad/theme_settings.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) { - QString output = input; - 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) ? 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); -} - -FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { - forceOpenDescriptions = forceOpen; - - QStackedLayout *themesLayout = new QStackedLayout(); - addItem(themesLayout); - - FrogPilotListWidget *themesList = new FrogPilotListWidget(this); - - ScrollView *themesPanel = new ScrollView(themesList, this); - - themesLayout->addWidget(themesPanel); - - FrogPilotListWidget *customThemesList = new FrogPilotListWidget(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 Pond\" to create and share your own themes!"), "../../frogpilot/assets/toggle_icons/icon_frog.png"}, - {"ColorScheme", tr("Color Scheme"), tr("The color scheme used throughout openpilot. Use the \"Theme Maker\" in \"The Pond\" 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 Pond\" to create and share your own themes!"), ""}, - {"IconPack", tr("Icon Pack"), tr("The icon style used across openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, - {"SignalAnimation", tr("Turn Signal"), tr("Themed turn-signal animations. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, - {"SoundPack", tr("Sound Pack"), tr("The sound pack used by openpilot. Use the \"Theme Maker\" in \"The Pond\" 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 Pond\" 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."), "../../frogpilot/assets/toggle_icons/icon_calendar.png"}, - {"RainbowPath", tr("Rainbow Path"), tr("Color the driving path like a Mario Kartโ€“style \"Rainbow Road\"."), "../../frogpilot/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!"), "../../frogpilot/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."), "../../frogpilot/assets/toggle_icons/icon_random_themes.png"}, - {"StartupAlert", tr("Startup Alert"), tr("Customize the \"Startup Alert\" message shown at the start of each drive."), "../../frogpilot/assets/toggle_icons/icon_message.png"} - }; - - for (const auto &[param, title, desc, icon] : themeToggles) { - AbstractControl *themeToggle; - - if (param == "CustomThemes") { - FrogPilotManageControl *personalizeOpenpilotToggle = new FrogPilotManageControl(param, title, desc, icon); - QObject::connect(personalizeOpenpilotToggle, &FrogPilotManageControl::manageButtonClicked, [customThemesPanel, themesLayout]() { - themesLayout->setCurrentWidget(customThemesPanel); - }); - themeToggle = personalizeOpenpilotToggle; - } else if (param == "ColorScheme") { - manageColorSchemeButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageColorSchemeButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageDistanceIconPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageIconPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageSignalAnimationButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageSoundPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); - QObject::connect(manageWheelIconsButton, &FrogPilotButtonsControl::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 FrogPilotButtonToggleControl(param, title, desc, icon, randomThemesToggles, randomThemesToggleNames); - - } else if (param == "StartupAlert") { - FrogPilotButtonsControl *startupAlertButton = new FrogPilotButtonsControl(title, desc, icon, {tr("STOCK"), tr("FROGPILOT"), 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 frogpilotTop = "Hop in and buckle up!"; - QString frogpilotBottom = "Human-tested, frog-approved ๐Ÿธ"; - - if (currentTop == stockTop && currentBottom == stockBottom) { - startupAlertButton->setCheckedButton(0); - } else if (currentTop == frogpilotTop && currentBottom == frogpilotBottom) { - startupAlertButton->setCheckedButton(1); - } else if (!currentTop.isEmpty() || !currentBottom.isEmpty()) { - startupAlertButton->setCheckedButton(2); - } - - QObject::connect(startupAlertButton, &FrogPilotButtonsControl::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", frogpilotTop.toStdString()); - params.put("StartupMessageBottom", frogpilotBottom.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 (FrogPilotConfirmationDialog::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 (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(themeToggle)) { - QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::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, &FrogPilotThemesPanel::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, &FrogPilotSettingsWindow::closeSubPanel, [themesLayout, themesPanel, this] { - openDescriptions(forceOpenDescriptions, toggles); - themesLayout->setCurrentWidget(themesPanel); - }); - QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotThemesPanel::updateState); -} - -void FrogPilotThemesPanel::showEvent(QShowEvent *event) { - 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 FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { - if (!isVisible() || finalizingDownload) { - return; - } - - const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_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]() { - colorDownloading = false; - distanceIconDownloading = false; - finalizingDownload = false; - iconDownloading = false; - signalDownloading = false; - soundDownloading = false; - themeDownloading = false; - wheelDownloading = false; - - 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 || frogpilot_scene.parked || parent->isFrogsGoMoo; - - manageColorSchemeButton->setText(1, colorDownloading ? tr("CANCEL") : tr("DOWNLOAD")); - manageColorSchemeButton->setEnabledButtons(0, !themeDownloading); - manageColorSchemeButton->setEnabledButtons(1, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_scene.online && (!themeDownloading || wheelDownloading) && !cancellingDownload && !finalizingDownload && !wheelsDownloaded && parked); - manageWheelIconsButton->setEnabledButtons(2, !themeDownloading); - - parent->keepScreenOn = themeDownloading; -} - -void FrogPilotThemesPanel::updateToggles() { - for (auto &[key, toggle] : toggles) { - if (parentKeys.contains(key)) { - toggle->setVisible(false); - } - } - - for (auto &[key, toggle] : toggles) { - if (parentKeys.contains(key)) { - continue; - } - - bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); - - 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(); -} +#include "frogpilot/ui/qt/offroad/theme_settings.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) { + 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); +} + +FrogPilotThemesPanel::FrogPilotThemesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + QStackedLayout *themesLayout = new QStackedLayout(); + addItem(themesLayout); + + FrogPilotListWidget *themesList = new FrogPilotListWidget(this); + + ScrollView *themesPanel = new ScrollView(themesList, this); + + themesLayout->addWidget(themesPanel); + + FrogPilotListWidget *customThemesList = new FrogPilotListWidget(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 Pond\" to create and share your own themes!"), "../../frogpilot/assets/toggle_icons/icon_frog.png"}, + {"ColorScheme", tr("Color Scheme"), tr("The color scheme used throughout openpilot. Use the \"Theme Maker\" in \"The Pond\" 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 Pond\" to create and share your own themes!"), ""}, + {"IconPack", tr("Icon Pack"), tr("The icon style used across openpilot. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"SignalAnimation", tr("Turn Signal"), tr("Themed turn-signal animations. Use the \"Theme Maker\" in \"The Pond\" to create and share your own themes!"), ""}, + {"SoundPack", tr("Sound Pack"), tr("The sound pack used by openpilot. Use the \"Theme Maker\" in \"The Pond\" 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 Pond\" 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."), "../../frogpilot/assets/toggle_icons/icon_calendar.png"}, + {"RainbowPath", tr("Rainbow Path"), tr("Color the driving path like a Mario Kartโ€“style \"Rainbow Road\"."), "../../frogpilot/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!"), "../../frogpilot/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."), "../../frogpilot/assets/toggle_icons/icon_random_themes.png"}, + {"StartupAlert", tr("Startup Alert"), tr("Customize the \"Startup Alert\" message shown at the start of each drive."), "../../frogpilot/assets/toggle_icons/icon_message.png"} + }; + + for (const auto &[param, title, desc, icon] : themeToggles) { + AbstractControl *themeToggle; + + if (param == "CustomThemes") { + FrogPilotManageControl *personalizeOpenpilotToggle = new FrogPilotManageControl(param, title, desc, icon); + QObject::connect(personalizeOpenpilotToggle, &FrogPilotManageControl::manageButtonClicked, [customThemesPanel, themesLayout]() { + themesLayout->setCurrentWidget(customThemesPanel); + }); + themeToggle = personalizeOpenpilotToggle; + } else if (param == "ColorScheme") { + manageColorSchemeButton = new FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageColorSchemeButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageDistanceIconPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageIconPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageSignalAnimationButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageSoundPackButton, &FrogPilotButtonsControl::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 FrogPilotButtonsControl(title, desc, icon, {tr("DELETE"), tr("DOWNLOAD"), tr("SELECT")}); + QObject::connect(manageWheelIconsButton, &FrogPilotButtonsControl::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 FrogPilotButtonToggleControl(param, title, desc, icon, randomThemesToggles, randomThemesToggleNames); + + } else if (param == "StartupAlert") { + FrogPilotButtonsControl *startupAlertButton = new FrogPilotButtonsControl(title, desc, icon, {tr("STOCK"), tr("FROGPILOT"), 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 frogpilotTop = "Hop in and buckle up!"; + QString frogpilotBottom = "Human-tested, frog-approved ๐Ÿธ"; + + if (currentTop == stockTop && currentBottom == stockBottom) { + startupAlertButton->setCheckedButton(0); + } else if (currentTop == frogpilotTop && currentBottom == frogpilotBottom) { + startupAlertButton->setCheckedButton(1); + } else if (!currentTop.isEmpty() || !currentBottom.isEmpty()) { + startupAlertButton->setCheckedButton(2); + } + + QObject::connect(startupAlertButton, &FrogPilotButtonsControl::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", frogpilotTop.toStdString()); + params.put("StartupMessageBottom", frogpilotBottom.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 (FrogPilotConfirmationDialog::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 (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(themeToggle)) { + QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::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, &FrogPilotThemesPanel::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, &FrogPilotSettingsWindow::closeSubPanel, [themesLayout, themesPanel, this] { + openDescriptions(forceOpenDescriptions, toggles); + themesLayout->setCurrentWidget(themesPanel); + }); + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotThemesPanel::updateState); +} + +void FrogPilotThemesPanel::showEvent(QShowEvent *event) { + 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 FrogPilotThemesPanel::updateState(const UIState &s, const FrogPilotUIState &fs) { + if (!isVisible() || finalizingDownload) { + return; + } + + const FrogPilotUIScene &frogpilot_scene = fs.frogpilot_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]() { + colorDownloading = false; + distanceIconDownloading = false; + finalizingDownload = false; + iconDownloading = false; + signalDownloading = false; + soundDownloading = false; + themeDownloading = false; + wheelDownloading = false; + + 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 || frogpilot_scene.parked || parent->isFrogsGoMoo; + + manageColorSchemeButton->setText(1, colorDownloading ? tr("CANCEL") : tr("DOWNLOAD")); + manageColorSchemeButton->setEnabledButtons(0, !themeDownloading); + manageColorSchemeButton->setEnabledButtons(1, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_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, frogpilot_scene.online && (!themeDownloading || wheelDownloading) && !cancellingDownload && !finalizingDownload && !wheelsDownloaded && parked); + manageWheelIconsButton->setEnabledButtons(2, !themeDownloading); + + parent->keepScreenOn = themeDownloading; +} + +void FrogPilotThemesPanel::updateToggles() { + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + toggle->setVisible(false); + } + } + + for (auto &[key, toggle] : toggles) { + if (parentKeys.contains(key)) { + continue; + } + + bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); + + 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(); +} diff --git a/frogpilot/ui/qt/widgets/frogpilot_controls.h b/frogpilot/ui/qt/widgets/frogpilot_controls.h index c2a1a414..44cc39bc 100644 --- a/frogpilot/ui/qt/widgets/frogpilot_controls.h +++ b/frogpilot/ui/qt/widgets/frogpilot_controls.h @@ -68,7 +68,7 @@ class FrogPilotListWidget : public QWidget { outer_layout.addStretch(); } inline void addItem(QWidget *w, bool expanding = false) { - w->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Fixed); + w->setSizePolicy(QSizePolicy::Preferred, expanding ? QSizePolicy::Expanding : QSizePolicy::Maximum); inner_layout.addWidget(w); } inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } diff --git a/tools/lib/api.py b/tools/lib/api.py index 2780481d..db5f94cd 100644 --- a/tools/lib/api.py +++ b/tools/lib/api.py @@ -3,7 +3,7 @@ import requests from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server -API_HOST = os.getenv('API_HOST', f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}') +API_HOST = os.getenv('API_HOST', f"https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}") # TODO: this should be merged into common.api diff --git a/tools/lib/auth.py b/tools/lib/auth.py index 39a9e04a..2e1de97c 100755 --- a/tools/lib/auth.py +++ b/tools/lib/auth.py @@ -35,7 +35,7 @@ from openpilot.tools.lib.auth_config import set_token, get_token from openpilot.frogpilot.common.frogpilot_utilities import use_konik_server -API_HOST = os.getenv('API_HOST', f'https://api.{'konik' if use_konik_server() else 'comma'}.ai') +API_HOST = os.getenv('API_HOST', f"https://api.{'konik' if use_konik_server() else 'comma'}.ai") PORT = 3000 diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py index c1dbe4f7..40e4a316 100755 --- a/tools/scripts/fetch_image_from_route.py +++ b/tools/scripts/fetch_image_from_route.py @@ -26,7 +26,7 @@ segment = int(sys.argv[2]) frame = int(sys.argv[3]) camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras" -url = f'https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}/v1/route/{route}/files' +url = f"https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}/v1/route/{route}/files" r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10) assert r.status_code == 200 print("got api response")