From 3ab8bae3f1171b8221476fea4fbd2727c1e0e7ba Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 8 May 2026 00:15:59 +0200 Subject: [PATCH 1/5] Made basic settings expanded by default. --- frontend/source/frontend/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/source/frontend/settings.cpp b/frontend/source/frontend/settings.cpp index 4ea76229..9ffe7933 100644 --- a/frontend/source/frontend/settings.cpp +++ b/frontend/source/frontend/settings.cpp @@ -62,7 +62,7 @@ struct Settings::Implementation struct SessionCollapsibles { - Nui::Observed overarchingSettings{true}; + Nui::Observed overarchingSettings{false}; Nui::Observed sshOptions{true}; Nui::Observed sftpOptions{true}; Nui::Observed terminalOptions{true}; From 3883211ffac7b14e31f60c8c552bf6cd589b126d Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 8 May 2026 00:16:06 +0200 Subject: [PATCH 2/5] Fixed formatting. --- frontend/source/frontend/settings.cpp | 73 +++++++++++++++------------ 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/frontend/source/frontend/settings.cpp b/frontend/source/frontend/settings.cpp index 9ffe7933..6d88c319 100644 --- a/frontend/source/frontend/settings.cpp +++ b/frontend/source/frontend/settings.cpp @@ -125,8 +125,7 @@ struct Settings::Implementation .languageCode = generalSettings.localization.language.value(), .logDirectory = generalSettings.logOptions.logDirectory.value(), .disableFileLogging = generalSettings.logOptions.disableFileLogging.value(), - .temporaryDownloadsDirectory = - generalSettings.localFilesystemOptions.temporaryDownloadsDirectory.value(), + .temporaryDownloadsDirectory = generalSettings.localFilesystemOptions.temporaryDownloadsDirectory.value(), .concurrency = sftpOptions.concurrency.value(), }; requiresRestart = false; @@ -136,13 +135,12 @@ struct Settings::Implementation { if (applyingToUi) return; - requiresRestart = - generalSettings.localization.language.value() != restartBaseline.languageCode - || generalSettings.logOptions.logDirectory.value() != restartBaseline.logDirectory - || generalSettings.logOptions.disableFileLogging.value() != restartBaseline.disableFileLogging - || generalSettings.localFilesystemOptions.temporaryDownloadsDirectory.value() - != restartBaseline.temporaryDownloadsDirectory - || sftpOptions.concurrency.value() != restartBaseline.concurrency; + requiresRestart = generalSettings.localization.language.value() != restartBaseline.languageCode || + generalSettings.logOptions.logDirectory.value() != restartBaseline.logDirectory || + generalSettings.logOptions.disableFileLogging.value() != restartBaseline.disableFileLogging || + generalSettings.localFilesystemOptions.temporaryDownloadsDirectory.value() != + restartBaseline.temporaryDownloadsDirectory || + sftpOptions.concurrency.value() != restartBaseline.concurrency; } Implementation( @@ -264,13 +262,16 @@ Settings::Settings( // (void0ArgsFunctor called with 1 argument). auto raf = Nui::val::global("requestAnimationFrame"); raf(Nui::bind( - [raf, this](Nui::val) { + [raf, this](Nui::val) + { raf(Nui::bind( - [raf, this](Nui::val) { + [raf, this](Nui::val) + { impl_->wasInitiallyLoaded = true; Nui::globalEventContext.executeActiveEventsImmediately(); raf(Nui::bind( - [this](Nui::val) { + [this](Nui::val) + { impl_->initialLoadDone = true; Nui::globalEventContext.executeActiveEventsImmediately(); }, @@ -309,7 +310,8 @@ Settings::Settings( auto attempts = std::make_shared(0); static constexpr int maxAttempts = 30; auto tryLocate = std::make_shared>(); - *tryLocate = [this, idCopy, attempts, tryLocate]() { + *tryLocate = [this, idCopy, attempts, tryLocate]() + { auto document = Nui::val::global("document"); auto element = document.call("getElementById", idCopy); if (element.isNull() || element.isUndefined()) @@ -317,24 +319,24 @@ Settings::Settings( if (++(*attempts) >= maxAttempts) { Log::warn( - "requestedSettingScrollId: no element with id '{}' after {} frames", - idCopy, maxAttempts + "requestedSettingScrollId: no element with id '{}' after {} frames", idCopy, maxAttempts ); return; } - Nui::val::global("requestAnimationFrame")( - Nui::bind([tryLocate](Nui::val) { (*tryLocate)(); }, std::placeholders::_1) - ); + Nui::val::global("requestAnimationFrame")(Nui::bind( + [tryLocate](Nui::val) + { + (*tryLocate)(); + }, + std::placeholders::_1 + )); return; } auto section = element.call("closest", std::string{"[data-settings-section]"}); if (section.isNull() || section.isUndefined()) { - Log::warn( - "requestedSettingScrollId: element '{}' has no [data-settings-section] ancestor", - idCopy - ); + Log::warn("requestedSettingScrollId: element '{}' has no [data-settings-section] ancestor", idCopy); return; } @@ -354,18 +356,24 @@ Settings::Settings( // Expand any collapsed settings-group ancestors so the target // isn't hidden behind a max-height:0 group. - Nui::val::global("addressableSettings") - .call("expandCollapsedGroupsContaining", idCopy); + Nui::val::global("addressableSettings").call("expandCollapsedGroupsContaining", idCopy); // rAF twice so the section display-swap + any group expansion // have painted before scroll measurement. auto raf = Nui::val::global("requestAnimationFrame"); - raf(Nui::bind([raf, idCopy](Nui::val) { - raf(Nui::bind([idCopy](Nui::val) { - Nui::val::global("addressableSettings") - .call("scrollToAndHighlight", idCopy); - }, std::placeholders::_1)); - }, std::placeholders::_1)); + raf(Nui::bind( + [raf, idCopy](Nui::val) + { + raf(Nui::bind( + [idCopy](Nui::val) + { + Nui::val::global("addressableSettings").call("scrollToAndHighlight", idCopy); + }, + std::placeholders::_1 + )); + }, + std::placeholders::_1 + )); }; (*tryLocate)(); } @@ -451,7 +459,10 @@ void Settings::applySettingsToUi() return; impl_->applyingToUi = true; - Nui::ScopeExit clearApplyingFlag{[this]() noexcept { impl_->applyingToUi = false; }}; + Nui::ScopeExit clearApplyingFlag{[this]() noexcept + { + impl_->applyingToUi = false; + }}; impl_->sessionSelectors.value().clear(); for (auto const& [sessionId, session] : impl_->stateHolder->stateCache().sessions) From 8ccfec639a1d2258a6ae2a5c63f1f3aef7778da7 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 8 May 2026 01:27:38 +0200 Subject: [PATCH 3/5] Added onboarding flow. --- _cmake/copy_styles_to_build_dir.cmake | 3 + dependencies/5cript-nui-components | 2 +- .../frontend/onboarding/onboarding.hpp | 54 +++++ frontend/source/frontend/CMakeLists.txt | 2 + frontend/source/frontend/main_page.cpp | 18 +- .../source/frontend/onboarding/onboarding.cpp | 184 ++++++++++++++++++ frontend/source/frontend/settings.cpp | 47 ++++- frontend/source/frontend/toolbar.cpp | 6 +- .../include/persistence/state/ui_options.hpp | 4 + static/assets/languages/de_DE.yaml | 8 + static/assets/languages/en_US.yaml | 10 +- static/styles/main.css | 1 + 12 files changed, 324 insertions(+), 15 deletions(-) create mode 100644 frontend/include/frontend/onboarding/onboarding.hpp create mode 100644 frontend/source/frontend/onboarding/onboarding.cpp diff --git a/_cmake/copy_styles_to_build_dir.cmake b/_cmake/copy_styles_to_build_dir.cmake index c86f3d41..5f68e1ae 100644 --- a/_cmake/copy_styles_to_build_dir.cmake +++ b/_cmake/copy_styles_to_build_dir.cmake @@ -13,6 +13,7 @@ if (NOT OMIT_FRONTEND_BUILD) "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/pagination.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/popup_menu.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/dropdown_menu.css" + "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/spotlight_overlay.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/tabs.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/tree.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/message_strip.css" @@ -32,6 +33,7 @@ if (NOT OMIT_FRONTEND_BUILD) COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/pagination.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/pagination.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/popup_menu.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/popup_menu.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/dropdown_menu.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/dropdown_menu.css" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/spotlight_overlay.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/spotlight_overlay.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/tabs.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/tabs.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/tree.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/tree.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/message_strip.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/message_strip.css" @@ -49,6 +51,7 @@ if (NOT OMIT_FRONTEND_BUILD) "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/pagination.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/popup_menu.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/dropdown_menu.css" + "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/spotlight_overlay.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/tabs.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/tree.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/message_strip.css" diff --git a/dependencies/5cript-nui-components b/dependencies/5cript-nui-components index 8c63fcd3..5c3bef77 160000 --- a/dependencies/5cript-nui-components +++ b/dependencies/5cript-nui-components @@ -1 +1 @@ -Subproject commit 8c63fcd31b83d6d19415d7654f6dda9a61ddd4a2 +Subproject commit 5c3bef77f105bc6f023f9537c8a3b2240046b28d diff --git a/frontend/include/frontend/onboarding/onboarding.hpp b/frontend/include/frontend/onboarding/onboarding.hpp new file mode 100644 index 00000000..da702af6 --- /dev/null +++ b/frontend/include/frontend/onboarding/onboarding.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace Frontend +{ + /** @brief Strongly-typed step identifier for the onboarding state + * machine. Adding a new step is as simple as appending an + * enumerator and wiring it in `Onboarding::Implementation`. */ + enum class OnboardingStep : unsigned + { + Inactive = 0, + OpenSettings = 1, + AddNewServer = 2, + Done = 3, + }; + + /** @brief DOM ids the onboarding orchestrator targets. The application + * attaches these ids to the actual buttons; keeping the + * constants here makes the binding explicit and grep-friendly. */ + namespace OnboardingTargets + { + inline constexpr char toolbarSettingsButtonId[] = "toolbar-settings-button"; + inline constexpr char settingsAddNewButtonId[] = "settings-add-new-button"; + } + + /** @brief First-launch onboarding flow. Auto-fires once when no servers + * are configured and the user has not previously completed or + * dismissed the flow. Persists the completion flag so it never + * reappears. */ + class Onboarding + { + public: + Onboarding(Persistence::StateHolder* stateHolder, FrontendEvents* events); + ROAR_PIMPL_SPECIAL_FUNCTIONS(Onboarding); + + /** @brief Evaluate trigger conditions and start the flow if they + * hold. Idempotent. Call from `MainPage::onSetupComplete` + * after persistence has loaded. */ + void maybeStart(); + + /** @brief Force-start the flow (debug / manual replay entry point). */ + void start(); + + private: + struct Implementation; + std::unique_ptr impl_; + }; +} // namespace Frontend diff --git a/frontend/source/frontend/CMakeLists.txt b/frontend/source/frontend/CMakeLists.txt index 8a865fea..d890cd29 100644 --- a/frontend/source/frontend/CMakeLists.txt +++ b/frontend/source/frontend/CMakeLists.txt @@ -141,6 +141,7 @@ target_sources( terminal/ssh_engine.cpp terminal/ssh_channel.cpp components/progress_bar.cpp + onboarding/onboarding.cpp file_explorer/side_model.cpp file_explorer/remote_side_model.cpp file_explorer/local_side_model.cpp @@ -289,6 +290,7 @@ nui_prepare_emscripten_target( "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/popup_menu.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/dropdown_menu.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/resizeable_table.css" + "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/spotlight_overlay.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/select.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/switch.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/tabs.css" diff --git a/frontend/source/frontend/main_page.cpp b/frontend/source/frontend/main_page.cpp index 3f8ec407..1dd7b1c8 100644 --- a/frontend/source/frontend/main_page.cpp +++ b/frontend/source/frontend/main_page.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ struct MainPage::Implementation SessionArea sessionArea; Settings settings; Licenses licenses; + Frontend::Onboarding onboarding; Nui::Observed darkMode; Nui::TimerHandle setupWait; @@ -54,6 +56,7 @@ struct MainPage::Implementation return sessionArea.getActiveSessionLayout(); }, newItemAskDialog, confirmDialog, multiInputDialog} , licenses{events} + , onboarding{stateHolder, events} , darkMode{true} , setupWait{} { @@ -83,16 +86,26 @@ void MainPage::onSetupComplete() auto showPersistenceWarning = [this, response]() { if (!response.hasOwnProperty("warning")) + { + impl_->onboarding.maybeStart(); return; + } const auto warning = response["warning"].as(); if (warning.empty()) + { + impl_->onboarding.maybeStart(); return; + } impl_->confirmDialog.open({ .styleVariant = ScriptNuiComponents::StyleVariant::Warning, .headerText = language->get("persistence", "warningLoadingState"), .text = fmt::format(fmt::runtime(language->get("persistence", "loadedWithWarnings")), warning), .buttons = ConfirmDialog::Button::Ok, .neverShowAgainId = "persistenceLoadWarning", + .onClose = [this](auto) + { + impl_->onboarding.maybeStart(); + }, }); }; @@ -103,7 +116,10 @@ void MainPage::onSetupComplete() .headerText = language->get("rootWarning", "header"), .text = language->get("rootWarning", "text"), .buttons = ConfirmDialog::Button::Ok, - .onClose = [showPersistenceWarning](auto) { showPersistenceWarning(); }, + .onClose = [showPersistenceWarning](auto) + { + showPersistenceWarning(); + }, }); return; } diff --git a/frontend/source/frontend/onboarding/onboarding.cpp b/frontend/source/frontend/onboarding/onboarding.cpp new file mode 100644 index 00000000..9fb3811e --- /dev/null +++ b/frontend/source/frontend/onboarding/onboarding.cpp @@ -0,0 +1,184 @@ +#include + +#include +#include + +#include + +#include + +namespace Frontend +{ + struct Onboarding::Implementation + { + Persistence::StateHolder* stateHolder; + FrontendEvents* events; + ScriptNuiComponents::SpotlightOverlay overlay; + Nui::ListenRemover settingsOpenListener{}; + Nui::ListenRemover newSessionListener{}; + OnboardingStep step{OnboardingStep::Inactive}; + bool persisted{false}; + + Implementation(Persistence::StateHolder* stateHolder, FrontendEvents* events) + : stateHolder{stateHolder} + , events{events} + {} + + bool shouldAutoStart() const + { + return !stateHolder->stateCache().uiOptions.onboardingCompleted; + } + + ScriptNuiComponents::SpotlightOptions buildStep1Options() + { + return ScriptNuiComponents::SpotlightOptions{ + .targetElementId = OnboardingTargets::toolbarSettingsButtonId, + .title = language->get("onboarding", "step1Title"), + .bodyText = language->get("onboarding", "step1Body"), + .stepCounter = ScriptNuiComponents::SpotlightStepCounter{1, 2}, + .ctaLabel = language->get("onboarding", "next"), + .skipLabel = language->get("onboarding", "skip"), + // Backdrop click is disabled: accidental clicks (or clicks + // forwarded through the cutout to the highlighted button) + // would otherwise terminate the flow rather than advance it. + // Dismiss paths remain via Skip, X, and Esc. + .dismissOnBackdropClick = false, + .onAdvance = + [this]() + { + advanceFromStep1(); + }, + .onDismiss = + [this]() + { + dismiss(); + }, + }; + } + + ScriptNuiComponents::SpotlightOptions buildStep2Options() + { + return ScriptNuiComponents::SpotlightOptions{ + .targetElementId = OnboardingTargets::settingsAddNewButtonId, + .title = language->get("onboarding", "step2Title"), + .bodyText = language->get("onboarding", "step2Body"), + .stepCounter = ScriptNuiComponents::SpotlightStepCounter{2, 2}, + .ctaLabel = language->get("onboarding", "finish"), + .skipLabel = language->get("onboarding", "skip"), + .dismissOnBackdropClick = false, + .onAdvance = + [this]() + { + finish(); + }, + .onDismiss = + [this]() + { + dismiss(); + }, + }; + } + + void enterStep1() + { + step = OnboardingStep::OpenSettings; + // The user clicking the actual highlighted button toggles + // settingsOpen — the listener picks that up and advances. + settingsOpenListener = Nui::smartListen( + events->settingsOpen, + [this](bool open) + { + if (open && step == OnboardingStep::OpenSettings) + enterStep2(); + } + ); + overlay.show(buildStep1Options()); + } + + void enterStep2() + { + step = OnboardingStep::AddNewServer; + // Clicking the highlighted "Add New" button writes a session + // name into onNewSession; advance when it changes. + newSessionListener = Nui::smartListen( + events->onNewSession, + [this](std::string const& sessionName) + { + if (!sessionName.empty() && step == OnboardingStep::AddNewServer) + finish(); + } + ); + overlay.show(buildStep2Options()); + } + + void advanceFromStep1() + { + // CTA path: open settings on the user's behalf if they used + // "Next" instead of clicking the highlighted button. + if (!events->settingsOpen.value()) + events->settingsOpen = true; + // The settings listener will pick that up and call enterStep2; + // no need to call it directly here. + } + + void finish() + { + persistFlag(); + shutdown(OnboardingStep::Done); + } + + void dismiss() + { + persistFlag(); + shutdown(OnboardingStep::Done); + } + + void shutdown(OnboardingStep terminal) + { + step = terminal; + settingsOpenListener = {}; + newSessionListener = {}; + overlay.hide(); + } + + void persistFlag() + { + if (persisted) + return; + persisted = true; + stateHolder->loadModifySave( + [](Persistence::State& state) + { + state.uiOptions.onboardingCompleted = true; + }, + [](std::optional const& error) + { + if (error) + Log::warn("Failed to persist onboarding completion flag: {}", *error); + } + ); + } + }; + + Onboarding::Onboarding(Persistence::StateHolder* stateHolder, FrontendEvents* events) + : impl_{std::make_unique(stateHolder, events)} + {} + + void Onboarding::maybeStart() + { + if (impl_->step != OnboardingStep::Inactive) + return; + if (!impl_->shouldAutoStart()) + return; + impl_->enterStep1(); + } + + void Onboarding::start() + { + if (impl_->step != OnboardingStep::Inactive) + return; + impl_->enterStep1(); + } + + ROAR_PIMPL_SPECIAL_FUNCTIONS_IMPL(Onboarding); +} // namespace Frontend diff --git a/frontend/source/frontend/settings.cpp b/frontend/source/frontend/settings.cpp index 6d88c319..7ccbda78 100644 --- a/frontend/source/frontend/settings.cpp +++ b/frontend/source/frontend/settings.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include @@ -935,7 +937,7 @@ void Settings::addNewSession() Persistence::SessionOptions::create(result.iconName, engineType); impl_->stateHolder->save( - [this, result](std::optional const& error) + [this, result, engineType](std::optional const& error) { if (error) { @@ -959,6 +961,19 @@ void Settings::addNewSession() ); impl_->events->onSettingsChanged.modify(); impl_->sessionSelectors.modifyNow(); + + // Activate the freshly-created session in the editor so + // the user lands directly in its config rather than + // having to click it in the side panel. + impl_->activeSection = Section::Session; + impl_->activeSession = result.sessionName; + loadSessionFromState(result.sessionName); + + // For SSH sessions, draw the eye to the Host/IP field — + // that's the first thing the user must fill in to make + // the new session functional. + if (engineType == Persistence::TerminalEngineType::ssh) + impl_->events->requestOpenSettingsAtId("session-host"); } ); }, @@ -1017,6 +1032,11 @@ Nui::ElementRenderer Settings::sectionSelector(SectionSelectorOptions const& opt return fmt::format("settings-page-section-selector {}", isActive(options) ? "active" : ""); } ), + // The onboarding overlay targets the "Add New" selector by id; + // other sections don't carry an id (kept off the wider DOM). + id = options.thisSection == Section::Add + ? std::string{Frontend::OnboardingTargets::settingsAddNewButtonId} + : std::string{}, onClick = [this, options]() { if (options.thisSection == Section::Add) { addNewSession(); @@ -1483,13 +1503,10 @@ Nui::ElementRenderer Settings::currentSession() impl_->currentSessionOptions.icon( language->getObserved("settings", "sessionOptions", "icon") ), - impl_->currentSessionOptions.orderBy( - language->getObserved("settings", "sessionOptions", "orderBy") - ), - impl_->currentSessionOptions.isStartupSession( - language->getObserved("settings", "sessionOptions", "isStartupSession") - ), - impl_->currentSessionOptions.layout(), + // SSH/local server options sit directly under the icon picker so + // the connection-relevant fields (host, port, etc.) are visible + // without scrolling. Sort/startup/layout controls live further + // down — they're rarely-touched session metadata. div{ class_ = "settings-visibility-box", style = observe(impl_->currentSessionOptions.terminalEngineType.state()).generate([](Persistence::TerminalEngineType type) { @@ -1505,7 +1522,10 @@ Nui::ElementRenderer Settings::currentSession() onChange(); }}, fragment( - impl_->currentSessionOptions.sshSessionOptions.host(language->getObserved("settings", "sessionOptions", "host")), + addressableSetting( + "session-host", + impl_->currentSessionOptions.sshSessionOptions.host(language->getObserved("settings", "sessionOptions", "host")) + ), impl_->currentSessionOptions.sshSessionOptions.port(language->getObserved("settings", "sessionOptions", "port")), impl_->currentSessionOptions.sshSessionOptions.user(language->getObserved("settings", "sessionOptions", "user")), impl_->currentSessionOptions.sshSessionOptions.sshKeyPrivate(language->getObserved("settings", "sessionOptions", "sshKeyPrivate")), @@ -1541,7 +1561,14 @@ Nui::ElementRenderer Settings::currentSession() language->getObserved("settings", "sessionOptions", "cleanEnvironment")) ) ) - ) + ), + impl_->currentSessionOptions.orderBy( + language->getObserved("settings", "sessionOptions", "orderBy") + ), + impl_->currentSessionOptions.isStartupSession( + language->getObserved("settings", "sessionOptions", "isStartupSession") + ), + impl_->currentSessionOptions.layout() ); return fragment( diff --git a/frontend/source/frontend/toolbar.cpp b/frontend/source/frontend/toolbar.cpp index a6201b11..480b136f 100644 --- a/frontend/source/frontend/toolbar.cpp +++ b/frontend/source/frontend/toolbar.cpp @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -111,8 +113,7 @@ Toolbar::Toolbar( DirectConnectDialog* directConnectDialog, ThemeController& themeController ) - : impl_(std::make_unique< - Implementation>(stateHolder, events, confirmDialog, directConnectDialog, themeController)) + : impl_(std::make_unique(stateHolder, events, confirmDialog, directConnectDialog, themeController)) { Log::info("Toolbar::Toolbar"); impl_->updateSessionsList( @@ -331,6 +332,7 @@ Nui::ElementRenderer Toolbar::operator()() Snc::button({ .icon = GeneratedSvgs::settings(), .attributes = { + id = std::string{Frontend::OnboardingTargets::toolbarSettingsButtonId}, onClick = [this]() { impl_->events->settingsOpen = true; }, diff --git a/persistence/include/persistence/state/ui_options.hpp b/persistence/include/persistence/state/ui_options.hpp index f5dbf13f..1b226d71 100644 --- a/persistence/include/persistence/state/ui_options.hpp +++ b/persistence/include/persistence/state/ui_options.hpp @@ -23,6 +23,9 @@ namespace Persistence int fileGridPageSize{500}; std::set neverShowAgainDialogs{}; std::vector localFavorites{}; + // Whether the first-launch onboarding flow has been completed + // (finished or dismissed). Set once; never auto-reset. + bool onboardingCompleted{false}; std::map fileGridExtensionIcons{ {".cpp", "icons/Development/noun-c-4921443.png"}, {".hpp", "icons/Development/noun-c-4921443.png"}, @@ -57,6 +60,7 @@ namespace Persistence fileGridPageSize, neverShowAgainDialogs, localFavorites, + onboardingCompleted, fileGridExtensionIcons) ) } \ No newline at end of file diff --git a/static/assets/languages/de_DE.yaml b/static/assets/languages/de_DE.yaml index 7b5f71df..77c89da7 100644 --- a/static/assets/languages/de_DE.yaml +++ b/static/assets/languages/de_DE.yaml @@ -688,3 +688,11 @@ directConnectDialog: sshKeyPrivateLabel: "Privater SSH-Schlüssel:" hostValidationMessage: "Host darf nicht leer sein." portValidationMessage: "Port muss eine Zahl zwischen 1 und 65535 sein." +onboarding: + step1Title: "Einstellungen öffnen" + step1Body: "Klicken Sie auf das Zahnrad-Symbol, um die Einstellungen zu öffnen. Dort konfigurieren Sie Ihren ersten SFTP-Server." + step2Title: "Neuen Server hinzufügen" + step2Body: "Wählen Sie „Neu hinzufügen“, um Ihre erste Serververbindung anzulegen. Host, Port, Benutzer und Schlüsseldateien werden für künftige Verbindungen gespeichert." + next: "Weiter" + finish: "Fertig" + skip: "Überspringen" diff --git a/static/assets/languages/en_US.yaml b/static/assets/languages/en_US.yaml index 5554abd9..7b3a9b66 100644 --- a/static/assets/languages/en_US.yaml +++ b/static/assets/languages/en_US.yaml @@ -684,4 +684,12 @@ directConnectDialog: userLabel: "User:" sshKeyPrivateLabel: "SSH Private Key:" hostValidationMessage: "Host must not be empty." - portValidationMessage: "Port must be a number between 1 and 65535." \ No newline at end of file + portValidationMessage: "Port must be a number between 1 and 65535." +onboarding: + step1Title: "Open Settings" + step1Body: "Click the gear icon to open Settings, where you can configure your first SFTP server." + step2Title: "Add a new server" + step2Body: "Use the Add New entry to create your first server connection. You can save host, port, user and key files for one-click reconnects." + next: "Next" + finish: "Finish" + skip: "Skip" \ No newline at end of file diff --git a/static/styles/main.css b/static/styles/main.css index d9592945..541b469d 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -32,6 +32,7 @@ @import "../../../styles/5cript-nui-components/popup_menu.css"; @import "../../../styles/5cript-nui-components/dropdown_menu.css"; @import "../../../styles/5cript-nui-components/resizeable_table.css"; +@import "../../../styles/5cript-nui-components/spotlight_overlay.css"; @import "../../../styles/5cript-nui-components/select.css"; @import "../../../styles/5cript-nui-components/switch.css"; @import "../../../styles/5cript-nui-components/tabs.css"; From 0ddd9bacf08691d4f9d8fa849cfd56c7e17157d0 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 8 May 2026 01:35:43 +0200 Subject: [PATCH 4/5] Moved second onboard step beyond first load. --- .../frontend/events/frontend_events.hpp | 6 ++++ .../frontend/onboarding/onboarding.hpp | 8 +++-- .../source/frontend/onboarding/onboarding.cpp | 36 +++++++++++++++++-- frontend/source/frontend/settings.cpp | 1 + 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/frontend/include/frontend/events/frontend_events.hpp b/frontend/include/frontend/events/frontend_events.hpp index 5e176a82..3113c5e5 100644 --- a/frontend/include/frontend/events/frontend_events.hpp +++ b/frontend/include/frontend/events/frontend_events.hpp @@ -16,6 +16,12 @@ struct FrontendEvents : public AppWideEvents Nui::Observed onNewSession{}; Nui::Observed onLayoutsChanged{false}; Nui::Observed settingsOpen{false}; + /// Latches to true the first time the Settings panel finishes its + /// initial 3-pass reveal (loader -> heavy subtree mount -> loader hidden). + /// Stays true for the rest of the session; consumers that need to wait + /// for the panel to be visually settled (e.g. onboarding step 2) can + /// gate on this rather than polling the DOM. + Nui::Observed settingsInitialLoadComplete{false}; Nui::Observed licensesOpen{false}; /// Opens settings and scrolls to the rendered element whose DOM id equals /// this string. Settings walks up from the element to find its diff --git a/frontend/include/frontend/onboarding/onboarding.hpp b/frontend/include/frontend/onboarding/onboarding.hpp index da702af6..09af215d 100644 --- a/frontend/include/frontend/onboarding/onboarding.hpp +++ b/frontend/include/frontend/onboarding/onboarding.hpp @@ -16,8 +16,12 @@ namespace Frontend { Inactive = 0, OpenSettings = 1, - AddNewServer = 2, - Done = 3, + // User clicked the Settings button; the spotlight is hidden while we + // wait for the panel's initial 3-pass reveal to finish so step 2 + // doesn't overlap with the loader. + WaitingForSettingsLoad = 2, + AddNewServer = 3, + Done = 4, }; /** @brief DOM ids the onboarding orchestrator targets. The application diff --git a/frontend/source/frontend/onboarding/onboarding.cpp b/frontend/source/frontend/onboarding/onboarding.cpp index 9fb3811e..48a517d9 100644 --- a/frontend/source/frontend/onboarding/onboarding.cpp +++ b/frontend/source/frontend/onboarding/onboarding.cpp @@ -16,6 +16,7 @@ namespace Frontend ScriptNuiComponents::SpotlightOverlay overlay; Nui::ListenRemover settingsOpenListener{}; Nui::ListenRemover newSessionListener{}; + Nui::ListenRemover settingsLoadedListener{}; OnboardingStep step{OnboardingStep::Inactive}; bool persisted{false}; @@ -89,15 +90,43 @@ namespace Frontend [this](bool open) { if (open && step == OnboardingStep::OpenSettings) - enterStep2(); + beginAwaitingSettingsLoad(); } ); overlay.show(buildStep1Options()); } + void beginAwaitingSettingsLoad() + { + step = OnboardingStep::WaitingForSettingsLoad; + // Hide the spotlight while the settings panel runs its 3-pass + // reveal (loader -> heavy subtree -> loader hidden). Showing + // step 2 over the loader is visually confusing because the + // target button is not yet visible. + overlay.hide(); + + // If a previous open already latched the load-complete signal + // (subsequent opens are instant), advance immediately. + if (events->settingsInitialLoadComplete.value()) + { + enterStep2(); + return; + } + + settingsLoadedListener = Nui::smartListen( + events->settingsInitialLoadComplete, + [this](bool loaded) + { + if (loaded && step == OnboardingStep::WaitingForSettingsLoad) + enterStep2(); + } + ); + } + void enterStep2() { step = OnboardingStep::AddNewServer; + settingsLoadedListener = {}; // Clicking the highlighted "Add New" button writes a session // name into onNewSession; advance when it changes. newSessionListener = Nui::smartListen( @@ -117,8 +146,8 @@ namespace Frontend // "Next" instead of clicking the highlighted button. if (!events->settingsOpen.value()) events->settingsOpen = true; - // The settings listener will pick that up and call enterStep2; - // no need to call it directly here. + // The settings listener will pick that up and call + // beginAwaitingSettingsLoad; no need to invoke it directly. } void finish() @@ -138,6 +167,7 @@ namespace Frontend step = terminal; settingsOpenListener = {}; newSessionListener = {}; + settingsLoadedListener = {}; overlay.hide(); } diff --git a/frontend/source/frontend/settings.cpp b/frontend/source/frontend/settings.cpp index 7ccbda78..79e508c7 100644 --- a/frontend/source/frontend/settings.cpp +++ b/frontend/source/frontend/settings.cpp @@ -275,6 +275,7 @@ Settings::Settings( [this](Nui::val) { impl_->initialLoadDone = true; + impl_->events->settingsInitialLoadComplete = true; Nui::globalEventContext.executeActiveEventsImmediately(); }, std::placeholders::_1 From e36ac9260544458173afd53648be7a9c4752c940 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 8 May 2026 01:43:29 +0200 Subject: [PATCH 5/5] Made onboarding complete when add new is clicked. --- .../frontend/events/frontend_events.hpp | 5 +++++ .../source/frontend/onboarding/onboarding.cpp | 18 ++++++++++-------- frontend/source/frontend/settings.cpp | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/include/frontend/events/frontend_events.hpp b/frontend/include/frontend/events/frontend_events.hpp index 3113c5e5..7ebc836e 100644 --- a/frontend/include/frontend/events/frontend_events.hpp +++ b/frontend/include/frontend/events/frontend_events.hpp @@ -14,6 +14,11 @@ struct FrontendEvents : public AppWideEvents {} Nui::Observed onNewSession{}; + /// Fires (via `.modify()`) when the user clicks the "Add New" entry in + /// the Settings side panel. Carries no payload — the new-session dialog + /// owns the actual creation flow; this is purely an intent signal for + /// observers like the onboarding orchestrator. + Nui::Observed onAddNewSessionRequested{false}; Nui::Observed onLayoutsChanged{false}; Nui::Observed settingsOpen{false}; /// Latches to true the first time the Settings panel finishes its diff --git a/frontend/source/frontend/onboarding/onboarding.cpp b/frontend/source/frontend/onboarding/onboarding.cpp index 48a517d9..d72d74eb 100644 --- a/frontend/source/frontend/onboarding/onboarding.cpp +++ b/frontend/source/frontend/onboarding/onboarding.cpp @@ -15,7 +15,7 @@ namespace Frontend FrontendEvents* events; ScriptNuiComponents::SpotlightOverlay overlay; Nui::ListenRemover settingsOpenListener{}; - Nui::ListenRemover newSessionListener{}; + Nui::ListenRemover addNewSessionListener{}; Nui::ListenRemover settingsLoadedListener{}; OnboardingStep step{OnboardingStep::Inactive}; bool persisted{false}; @@ -127,13 +127,15 @@ namespace Frontend { step = OnboardingStep::AddNewServer; settingsLoadedListener = {}; - // Clicking the highlighted "Add New" button writes a session - // name into onNewSession; advance when it changes. - newSessionListener = Nui::smartListen( - events->onNewSession, - [this](std::string const& sessionName) + // Clicking the highlighted "Add New" entry pulses + // onAddNewSessionRequested; finish on that signal regardless of + // whether the user goes on to confirm or cancel the dialog — + // they've already learned where the entry point is. + addNewSessionListener = Nui::smartListen( + events->onAddNewSessionRequested, + [this](bool) { - if (!sessionName.empty() && step == OnboardingStep::AddNewServer) + if (step == OnboardingStep::AddNewServer) finish(); } ); @@ -166,7 +168,7 @@ namespace Frontend { step = terminal; settingsOpenListener = {}; - newSessionListener = {}; + addNewSessionListener = {}; settingsLoadedListener = {}; overlay.hide(); } diff --git a/frontend/source/frontend/settings.cpp b/frontend/source/frontend/settings.cpp index 79e508c7..ba91e7e4 100644 --- a/frontend/source/frontend/settings.cpp +++ b/frontend/source/frontend/settings.cpp @@ -927,6 +927,7 @@ bool Settings::isActive(SectionSelectorOptions const& options) void Settings::addNewSession() { + impl_->events->onAddNewSessionRequested.modify(); impl_->newSessionDialog.open({ .onConfirm = [this](auto const& result) {