From 7e019125c5c2bf6615c61a4d2fa2f210744096b4 Mon Sep 17 00:00:00 2001 From: 5cript Date: Mon, 19 Oct 2026 21:36:51 +0200 Subject: [PATCH 1/2] Replaced the glass file tiles with a flat translucent style --- nui-file-explorer/styles/file_grid.css | 111 +++---------------------- static/styles/main.css | 17 ++-- 2 files changed, 23 insertions(+), 105 deletions(-) diff --git a/nui-file-explorer/styles/file_grid.css b/nui-file-explorer/styles/file_grid.css index a9baba53..29a7c10e 100644 --- a/nui-file-explorer/styles/file_grid.css +++ b/nui-file-explorer/styles/file_grid.css @@ -187,7 +187,6 @@ } } -/* ── Base: layout + outer shadow only; glass layers live in ::before/::after ── */ .nui-file-grid-item-icons { display: flex; flex-direction: column; @@ -198,14 +197,10 @@ text-align: center; font-weight: 600; - border-radius: 14px; - border: none; - background: transparent; + border-radius: 10px; + border: 1px solid var(--nui-file-grid-tile-border, rgba(255, 255, 255, 0.22)); + background: var(--nui-file-grid-tile-background, rgba(255, 255, 255, 0.10)); overflow: hidden; - isolation: isolate; - - /* WebKitGTK paints without compositing: blurry shadows and hover transitions repaint the whole grid */ - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.35); &[data-type="Symbolic Link"] { color: var(--nui-file-grid-symbolic-link-color, rgb(0, 242, 255)); @@ -234,42 +229,6 @@ } } -/* ── Liquid glass layer: blurred backdrop + turbulence displacement ── - `:not(.nui-file-grid-loading-placeholder)` keeps the glass effect off the - loading placeholder, which uses a flat grayscale plate instead. */ -.nui-file-grid-item-icons:not(.nui-file-grid-loading-placeholder)::after { - content: ''; - position: absolute; - z-index: -1; - inset: 0; - border-radius: 14px; - overflow: hidden; - isolation: isolate; - - backdrop-filter: blur(14px) saturate(160%); - -webkit-backdrop-filter: blur(14px) saturate(160%); - - background: - linear-gradient(145deg, - rgba(255, 255, 255, 0.32), - rgba(255, 255, 255, 0.05)); -} - -/* ── Specular highlight border ── */ -.nui-file-grid-item-icons:not(.nui-file-grid-loading-placeholder)::before { - content: ''; - position: absolute; - inset: 0; - z-index: 0; - border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.38); - box-shadow: - inset 2px 2px 0px -1px rgba(255, 255, 255, 0.75), - inset 0 0 4px 1px rgba(255, 255, 255, 0.45); - pointer-events: none; -} - -/* ── Content above pseudo layers ── */ .nui-file-grid-item-icons>* { position: relative; z-index: 1; @@ -277,35 +236,19 @@ .nui-file-grid-item-icons.is-muted { opacity: 0.55; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.45); filter: saturate(70%) brightness(85%); } -.nui-file-grid-item-icons.is-muted::after { - background: - linear-gradient(145deg, - rgba(255, 255, 255, 0.12), - rgba(255, 255, 255, 0.02)); -} - -.nui-file-grid-item-icons.is-muted::before { - border-color: rgba(255, 255, 255, 0.18); - box-shadow: - inset 1px 1px 0 rgba(255, 255, 255, 0.25), - inset -1px -1px 0 rgba(0, 0, 0, 0.25); -} - .nui-file-grid-item-icons.is-highlighted { position: relative; z-index: 1; } -/* Rainbow border overrides the specular ::before when highlighted */ .nui-file-grid-item-icons.is-highlighted::before { content: ""; position: absolute; - inset: 0; - border-radius: 14px; + inset: -1px; + border-radius: 10px; border: 2px solid transparent; @@ -331,33 +274,17 @@ } .nui-file-grid-item-icons:hover { - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.45); -} - -.nui-file-grid-item-icons:hover::before { - box-shadow: - inset 2px 2px 0px -1px rgba(255, 255, 255, 0.9), - inset 0 0 5px 1px rgba(255, 255, 255, 0.6); - border-color: rgba(255, 255, 255, 0.55); -} - -.nui-file-grid-item-icons:is(.selected)::after { - background: - linear-gradient(145deg, - rgba(255, 255, 255, 0.8), - rgba(255, 255, 255, 0.35)); + background: var(--nui-file-grid-tile-hover-background, rgba(255, 255, 255, 0.16)); + border-color: var(--nui-file-grid-tile-hover-border, rgba(255, 255, 255, 0.35)); } -.nui-file-grid-item-icons:is(.selected)::before { - border: 1px solid var(--nui-file-grid-border-color); +.nui-file-grid-item-icons.selected { + background: var(--nui-file-grid-item-highlight, #3d2cd6); + border-color: var(--nui-file-grid-tile-hover-border, rgba(255, 255, 255, 0.35)); } -.nui-file-grid-item-icons.drop-hovered::after { - background: - linear-gradient(145deg, - hsla(241, 82%, 49%, 0.55), - rgba(255, 255, 255, 0.05)); +.nui-file-grid-item-icons.drop-hovered { + background: hsla(241, 82%, 49%, 0.45); } .nui-file-grid-head { @@ -768,21 +695,9 @@ to { transform: rotate(360deg); } } -/* Flat grayscale plate — override the glass-styling pseudos inherited from - .nui-file-grid-item-icons. Colors come from theme-aware variables defined in main.css. */ .nui-file-grid-item-icons.nui-file-grid-loading-placeholder { - box-shadow: none; -} - -.nui-file-grid-item-icons.nui-file-grid-loading-placeholder::after { - backdrop-filter: none; - -webkit-backdrop-filter: none; background: var(--nui-file-grid-loading-placeholder-background); -} - -.nui-file-grid-item-icons.nui-file-grid-loading-placeholder::before { - border: 1px solid var(--nui-file-grid-loading-placeholder-border); - box-shadow: none; + border-color: var(--nui-file-grid-loading-placeholder-border); } .nui-file-grid-table-row.nui-file-grid-loading-placeholder .nui-file-grid-table-cell { diff --git a/static/styles/main.css b/static/styles/main.css index acd519cc..a7ecea45 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -189,6 +189,11 @@ --nui-file-grid-item-highlight: color-mix(in srgb, var(--theme-color) 80%, var(--brightness-mixin) var(--brightness-mixin-percent)); --nui-file-grid-symbolic-link-color: color-mix(in srgb, var(--theme-color) 80%, var(--anti-brightness-mixin) var(--anti-brightness-mixin-percent)); + --nui-file-grid-tile-background: rgba(255, 255, 255, 0.10); + --nui-file-grid-tile-border: rgba(255, 255, 255, 0.22); + --nui-file-grid-tile-hover-background: rgba(255, 255, 255, 0.16); + --nui-file-grid-tile-hover-border: rgba(255, 255, 255, 0.35); + /* Loading placeholder — flat grayscale plate with slight transparency (dark default). */ --nui-file-grid-loading-placeholder-background: rgba(255, 255, 255, 0.08); --nui-file-grid-loading-placeholder-border: rgba(255, 255, 255, 0.18); @@ -227,13 +232,6 @@ background-image: var(--background-image, url("data-url:../../../styles/nui-file-explorer/file_grid_background_light.png")); } - & .nui-file-grid-item-icons:is(.selected)::after { - background: - linear-gradient(145deg, - color-mix(in srgb, var(--theme-color) 80%, rgba(0, 0, 0, 0.1)), - color-mix(in srgb, var(--theme-color) 80%, rgba(0, 0, 0, 0.3))); - } - --script-nui-components-switch-color-inactive: var(--border-color-fields); --script-nui-components-checkbox-border-color: var(--anti-brightness-mixin); @@ -242,6 +240,11 @@ --nui-file-grid-symbolic-link-color: color-mix(in srgb, var(--theme-color) 80%, var(--anti-brightness-mixin) var(--anti-brightness-mixin-percent)); --nui-file-grid-places-highlight-contrast: var(--color); + --nui-file-grid-tile-background: rgba(255, 255, 255, 0.55); + --nui-file-grid-tile-border: rgba(0, 0, 0, 0.14); + --nui-file-grid-tile-hover-background: rgba(255, 255, 255, 0.75); + --nui-file-grid-tile-hover-border: rgba(0, 0, 0, 0.28); + /* Loading placeholder (light mode). */ --nui-file-grid-loading-placeholder-background: rgba(0, 0, 0, 0.05); --nui-file-grid-loading-placeholder-border: rgba(0, 0, 0, 0.15); From 763d4eaf071a51646ad8f13c6646c7ab9417147a Mon Sep 17 00:00:00 2001 From: 5cript Date: Mon, 19 Oct 2026 22:03:42 +0200 Subject: [PATCH 2/2] Marked the file explorer sides as local or remote - A card on top of Places names the machine with user and host, a chip in the path bar says Local or Remote (#244) - New RpcSystem::getHostname for the local side; the remote side shows user@host:port --- backend/include/backend/rpc_system.hpp | 3 + backend/source/backend/rpc_system.cpp | 30 ++++++ .../file_explorer_panel.hpp | 6 ++ .../file_explorer/file_explorer_texts.cpp | 5 + frontend/source/frontend/session.cpp | 32 +++--- .../file_explorer_panel.cpp | 47 +++++++++ .../include/nui-file-explorer/side.hpp | 9 ++ .../side/side_implementation.hpp | 1 + .../nui-file-explorer/side/side_settings.hpp | 16 +++ .../include/nui-file-explorer/texts.hpp | 7 ++ .../source/nui-file-explorer/side.cpp | 97 +++++++++++++++++++ nui-file-explorer/styles/file_grid.css | 69 +++++++++++++ static/assets/languages/de_DE.yaml | 3 + static/assets/languages/en_US.yaml | 3 + static/styles/main.css | 4 + 15 files changed, 320 insertions(+), 12 deletions(-) diff --git a/backend/include/backend/rpc_system.hpp b/backend/include/backend/rpc_system.hpp index 3a289785..8443f2cf 100644 --- a/backend/include/backend/rpc_system.hpp +++ b/backend/include/backend/rpc_system.hpp @@ -10,8 +10,11 @@ class RpcSystem : public RpcHelper::StrandRpc private: void registerGetUsername(); + void registerGetHostname(); std::string username() const; + std::string hostname() const; mutable std::string usernameMemo_; + mutable std::string hostnameMemo_; }; \ No newline at end of file diff --git a/backend/source/backend/rpc_system.cpp b/backend/source/backend/rpc_system.cpp index ecc983c5..3f688990 100644 --- a/backend/source/backend/rpc_system.cpp +++ b/backend/source/backend/rpc_system.cpp @@ -2,6 +2,8 @@ #include #include +#include + #ifdef _WIN32 # include # include @@ -26,6 +28,7 @@ RpcSystem::RpcSystem(boost::asio::any_io_executor executor, Nui::Window& wnd, Nu : RpcHelper::StrandRpc{executor, wnd, hub} { registerGetUsername(); + registerGetHostname(); } void RpcSystem::registerGetUsername() @@ -39,6 +42,33 @@ void RpcSystem::registerGetUsername() ); } +void RpcSystem::registerGetHostname() +{ + on("RpcSystem::getHostname") + .perform( + [this](RpcHelper::RpcOnce&& reply) + { + reply({{"hostname", hostname()}, {"success", true}}); + } + ); +} + +std::string RpcSystem::hostname() const +{ + if (!hostnameMemo_.empty()) + return hostnameMemo_; + + boost::system::error_code error; + auto name = boost::asio::ip::host_name(error); + if (error) + { + Log::error("RpcSystem::hostname: {}", error.message()); + return "unknown"; + } + hostnameMemo_ = Paths::sanitizedUtf8(name); + return hostnameMemo_; +} + std::string RpcSystem::username() const { if (!usernameMemo_.empty()) diff --git a/frontend/include/frontend/session_components/file_explorer_panel.hpp b/frontend/include/frontend/session_components/file_explorer_panel.hpp index b4db7699..b627e74c 100644 --- a/frontend/include/frontend/session_components/file_explorer_panel.hpp +++ b/frontend/include/frontend/session_components/file_explorer_panel.hpp @@ -90,6 +90,12 @@ class FileExplorerPanel */ void setup(); + /** + * @brief Sets the user, host and port the remote side names as its origin. Syncing the change is left + * to the caller. + */ + void setRemoteOrigin(std::string userAndHost); + /** * @brief Attaches the caller-provided synchronize callback to both sides * of the grid. Used by Session::setupFileGrid to wire in the diff --git a/frontend/source/frontend/file_explorer/file_explorer_texts.cpp b/frontend/source/frontend/file_explorer/file_explorer_texts.cpp index 04adf2ce..3d722789 100644 --- a/frontend/source/frontend/file_explorer/file_explorer_texts.cpp +++ b/frontend/source/frontend/file_explorer/file_explorer_texts.cpp @@ -54,6 +54,11 @@ NuiFileExplorer::Texts fileExplorerTexts() .synchronize = language->get("fileExplorer", "contextMenu", "synchronize"), .synchronizeHint = language->get("fileExplorer", "contextMenu", "synchronizeHint"), }, + .origin = + { + .local = language->get("fileExplorer", "origin", "local"), + .remote = language->get("fileExplorer", "origin", "remote"), + }, }; } diff --git a/frontend/source/frontend/session.cpp b/frontend/source/frontend/session.cpp index 6a495b18..feb175cd 100644 --- a/frontend/source/frontend/session.cpp +++ b/frontend/source/frontend/session.cpp @@ -41,6 +41,20 @@ using namespace Nui; using namespace Nui::Elements; using namespace Nui::Attributes; +namespace +{ + /** + * @brief Formats the address of an SSH session as user@host:port, with brackets around IPv6 hosts. + */ + std::string sshAddress(std::string const& user, Persistence::SshSessionOptions const& options) + { + auto host = options.host; + if (host.find(':') != std::string::npos) + host = "[" + host + "]"; + return fmt::format("{}@{}:{}", user, host, options.port.value_or(22)); + } +} + struct Session::Implementation { // Prop-drill @@ -802,11 +816,9 @@ void Session::onOpenSession(bool success, std::string const& info) { const auto& snapshot = impl_->snapshotManager->pending(); const auto& user = snapshot.remoteUsername; - auto host = sshSessionOptions.host; - const auto port = sshSessionOptions.port.value_or(22); - if (host.find(":") != std::string::npos) - host = "[" + host + "]"; - *impl_->tabTitle = impl_->disambiguateTitle(this, fmt::format("{}@{}:{}", user, host, port)); + const auto address = sshAddress(user, sshSessionOptions); + *impl_->tabTitle = impl_->disambiguateTitle(this, address); + impl_->fileExplorerPanel.setRemoteOrigin(address); impl_->snapshotManager->setRemoteUsername(user); const bool sftpWasOpen = snapshot.sftpWasOpen; if (sftpWasOpen) @@ -832,13 +844,9 @@ void Session::onOpenSession(bool success, std::string const& info) Log::info("Retrieved username: {}", username); const auto user = sshSessionOptions.loginUser().name.value_or(username); - auto host = sshSessionOptions.host; - const auto port = sshSessionOptions.port.value_or(22); - - // assume ipv6 when finding ':' in host - if (host.find(":") != std::string::npos) - host = "[" + host + "]"; - *impl_->tabTitle = impl_->disambiguateTitle(this, fmt::format("{}@{}:{}", user, host, port)); + const auto address = sshAddress(user, sshSessionOptions); + *impl_->tabTitle = impl_->disambiguateTitle(this, address); + impl_->fileExplorerPanel.setRemoteOrigin(address); impl_->snapshotManager->setRemoteUsername(user); openSftp(user); diff --git a/frontend/source/frontend/session_components/file_explorer_panel.cpp b/frontend/source/frontend/session_components/file_explorer_panel.cpp index 42bc4914..b7797554 100644 --- a/frontend/source/frontend/session_components/file_explorer_panel.cpp +++ b/frontend/source/frontend/session_components/file_explorer_panel.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,40 @@ using namespace Nui::Attributes; namespace { + std::string stringProperty(Nui::val const& response, char const* name) + { + if (!response.hasOwnProperty(name)) + return {}; + return response[name].as(); + } + + /** + * @brief Names the user and hostname of this machine as the origin of the local side. + * + * @param side The local side. + * @param alive Expires with the panel, so late replies are dropped. + */ + void requestLocalOrigin(NuiFileExplorer::Side* side, std::weak_ptr alive) + { + Nui::RpcClient::callWithBackChannel( + "RpcSystem::getUsername", + [side, alive](Nui::val usernameResponse) + { + Nui::RpcClient::callWithBackChannel( + "RpcSystem::getHostname", + [side, alive, username = stringProperty(usernameResponse, "username")](Nui::val hostnameResponse) + { + if (alive.expired()) + return; + const auto hostname = stringProperty(hostnameResponse, "hostname"); + side->originHost(username.empty() ? hostname : fmt::format("{}@{}", username, hostname)); + Nui::globalEventContext.executeActiveEventsImmediately(); + } + ); + } + ); + } + NuiFileExplorer::FileGrid buildFileGrid(FileExplorerPanel::Params& params, Persistence::SessionOptions const& engineOptions) { @@ -58,6 +93,7 @@ namespace }, .pageSize = uiOptions.fileGridPageSize, .texts = fileExplorerTexts(), + .origin = NuiFileExplorer::SideOrigin::Local, }, { .pathBarOnTop = uiOptions.fileGridPathBarOnTop, @@ -74,6 +110,7 @@ namespace }, .pageSize = uiOptions.fileGridPageSize, .texts = fileExplorerTexts(), + .origin = NuiFileExplorer::SideOrigin::Remote, }, std::make_unique( uiOptions, confirmDialog, inputDialog, filePropertyDialog, archiveTransferDialog @@ -106,6 +143,7 @@ namespace }, .pageSize = uiOptions.fileGridPageSize, .texts = fileExplorerTexts(), + .origin = NuiFileExplorer::SideOrigin::Local, }, std::make_unique( uiOptions, confirmDialog, inputDialog, filePropertyDialog, archiveTransferDialog @@ -130,6 +168,7 @@ struct FileExplorerPanel::Implementation NuiFileExplorer::FileGrid fileGrid; Nui::Observed> fileExplorerElement{}; LanguageChangeListener languageListener{}; + std::shared_ptr alive = std::make_shared(true); explicit Implementation(Params&& params) : stateHolder{params.stateHolder} @@ -231,6 +270,14 @@ void FileExplorerPanel::setup() { localSideModel().setRemoteModel(nullptr); } + + requestLocalOrigin(&localFileGridSide(), impl_->alive); +} + +void FileExplorerPanel::setRemoteOrigin(std::string userAndHost) +{ + if (auto* remote = remoteFileGridSide(); remote) + remote->originHost(std::move(userAndHost)); } void FileExplorerPanel::setOnSynchronize(std::function onSync) diff --git a/nui-file-explorer/include/nui-file-explorer/side.hpp b/nui-file-explorer/include/nui-file-explorer/side.hpp index 04f4dcea..fa1b5779 100644 --- a/nui-file-explorer/include/nui-file-explorer/side.hpp +++ b/nui-file-explorer/include/nui-file-explorer/side.hpp @@ -142,6 +142,11 @@ namespace NuiFileExplorer */ Texts const& texts() const; + /** + * @brief Sets the user and host the origin card names. Syncing the change is left to the caller. + */ + void originHost(std::string value); + /** * @brief Sets a callback invoked when both sides have exactly one directory-like item * selected and the user triggers the "Synchronize..." context menu entry. @@ -199,6 +204,10 @@ namespace NuiFileExplorer Nui::ElementRenderer operator()(); private: + Nui::ElementRenderer originIconAndKind(); + std::string originClass(std::string const& baseClass) const; + Nui::ElementRenderer originChip(); + Nui::ElementRenderer originCard(); Nui::ElementRenderer headMenu(); Nui::ElementRenderer pathBar(); Nui::ElementRenderer filter(); diff --git a/nui-file-explorer/include/nui-file-explorer/side/side_implementation.hpp b/nui-file-explorer/include/nui-file-explorer/side/side_implementation.hpp index e466f267..d81a4d1c 100644 --- a/nui-file-explorer/include/nui-file-explorer/side/side_implementation.hpp +++ b/nui-file-explorer/include/nui-file-explorer/side/side_implementation.hpp @@ -44,6 +44,7 @@ namespace NuiFileExplorer Nui::Observed> sorting{{SortCriterion::Name, true}}; Nui::Observed showHiddenFiles{false}; Nui::Observed texts{}; + Nui::Observed originHost{}; std::weak_ptr sideElement{}; std::weak_ptr scrollContainer{}; diff --git a/nui-file-explorer/include/nui-file-explorer/side/side_settings.hpp b/nui-file-explorer/include/nui-file-explorer/side/side_settings.hpp index fd5d82cd..c80b2793 100644 --- a/nui-file-explorer/include/nui-file-explorer/side/side_settings.hpp +++ b/nui-file-explorer/include/nui-file-explorer/side/side_settings.hpp @@ -3,9 +3,19 @@ #include #include +#include namespace NuiFileExplorer { + /** + * @brief Which machine a side shows, labelled in the path bar and on top of Places. + */ + enum class SideOrigin + { + Local, + Remote + }; + struct SideSettings { bool pathBarOnTop = false; @@ -18,5 +28,11 @@ namespace NuiFileExplorer // Texts the side renders, English unless translations are passed. Texts texts{}; + + /** + * @brief Shows the origin chip and card when set. The user and host are set through + * Side::originHost. + */ + std::optional origin{std::nullopt}; }; } diff --git a/nui-file-explorer/include/nui-file-explorer/texts.hpp b/nui-file-explorer/include/nui-file-explorer/texts.hpp index ece36930..c3848fab 100644 --- a/nui-file-explorer/include/nui-file-explorer/texts.hpp +++ b/nui-file-explorer/include/nui-file-explorer/texts.hpp @@ -65,6 +65,12 @@ namespace NuiFileExplorer std::string synchronizeHint{"Select exactly one directory on each side to synchronize."}; }; + struct Origin + { + std::string local{"Local"}; + std::string remote{"Remote"}; + }; + Toolbar toolbar{}; NewMenu newMenu{}; SortMenu sortMenu{}; @@ -72,5 +78,6 @@ namespace NuiFileExplorer Columns columns{}; Places places{}; ContextMenu contextMenu{}; + Origin origin{}; }; } diff --git a/nui-file-explorer/source/nui-file-explorer/side.cpp b/nui-file-explorer/source/nui-file-explorer/side.cpp index f8b320e5..fead3b77 100644 --- a/nui-file-explorer/source/nui-file-explorer/side.cpp +++ b/nui-file-explorer/source/nui-file-explorer/side.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include using namespace std::string_literals; @@ -712,6 +714,11 @@ namespace NuiFileExplorer return impl_->texts.value(); } + void Side::originHost(std::string value) + { + impl_->originHost = std::move(value); + } + void Side::setOnSynchronize( std::function callback ) @@ -735,6 +742,94 @@ namespace NuiFileExplorer return result; } + Nui::ElementRenderer Side::originIconAndKind() + { + using namespace Nui::Attributes; + using Nui::Elements::span; + using Nui::Elements::fragment; + + const bool isLocal = *impl_->settings.origin == SideOrigin::Local; + + // clang-format off + return fragment( + isLocal ? Ui5Icons::laptop() : Ui5Icons::it_host(), + span{class_ = "nui-file-grid-origin-kind"}( + observe(impl_->texts).generate([this, isLocal]() { + return isLocal ? impl_->texts.value().origin.local : impl_->texts.value().origin.remote; + }) + ) + ); + // clang-format on + } + + std::string Side::originClass(std::string const& baseClass) const + { + return fmt::format( + "{} {}", + baseClass, + *impl_->settings.origin == SideOrigin::Local ? "nui-file-grid-origin-local" : "nui-file-grid-origin-remote" + ); + } + + Nui::ElementRenderer Side::originChip() + { + using namespace Nui::Attributes; + using Nui::Elements::span; + + if (!impl_->settings.origin) + return Nui::nil(); + + // clang-format off + return span{ + class_ = originClass("nui-file-grid-origin-chip"), + Nui::Attributes::title = observe(impl_->originHost).generate([this]() { + return impl_->originHost.value(); + }), + }( + originIconAndKind() + ); + // clang-format on + } + + Nui::ElementRenderer Side::originCard() + { + using namespace Nui::Attributes; + using Nui::Elements::div; + + if (!impl_->settings.origin) + return Nui::nil(); + + // clang-format off + return div{ + class_ = originClass("nui-file-grid-origin-card"), + }( + div{class_ = "nui-file-grid-origin-card-kind"}( + originIconAndKind() + ), + div{ + class_ = "nui-file-grid-origin-host", + Nui::Attributes::title = observe(impl_->originHost).generate([this]() { + return impl_->originHost.value(); + }), + }( + observe(impl_->originHost), + [this]() -> Nui::ElementRenderer { + // User and host on their own lines keep both cards equally tall. + const auto noBreakSpace = std::string{" "}; + auto const& userAndHost = impl_->originHost.value(); + const auto at = userAndHost.find('@'); + const auto user = at == std::string::npos ? noBreakSpace : userAndHost.substr(0, at + 1); + const auto host = at == std::string::npos ? userAndHost : userAndHost.substr(at + 1); + return Nui::Elements::fragment( + div{}(user), + div{}(host.empty() ? noBreakSpace : host) + ); + } + ) + ); + // clang-format on + } + Nui::ElementRenderer Side::headMenu() { using namespace Nui; @@ -820,6 +915,7 @@ namespace NuiFileExplorer event.call("stopPropagation"); } }( + originCard(), (*places_)() ); // clang-format on @@ -937,6 +1033,7 @@ namespace NuiFileExplorer } } }), + originChip(), ScriptNuiComponents::textInput({ .value = "", .attributes = { diff --git a/nui-file-explorer/styles/file_grid.css b/nui-file-explorer/styles/file_grid.css index 29a7c10e..252f75f5 100644 --- a/nui-file-explorer/styles/file_grid.css +++ b/nui-file-explorer/styles/file_grid.css @@ -287,6 +287,75 @@ background: hsla(241, 82%, 49%, 0.45); } +.nui-file-grid-origin-local, +.nui-file-grid-origin-remote { + --nui-file-grid-origin-color: var(--nui-file-grid-origin-accent, var(--nui-file-grid-color)); + color: var(--nui-file-grid-origin-color); + border: 1px solid color-mix(in srgb, var(--nui-file-grid-origin-color) 60%, transparent); + border-radius: 4px; + white-space: nowrap; + + & svg { + width: 14px; + height: 14px; + flex: none; + fill: currentColor; + } +} + +.nui-file-grid-origin-kind { + font-weight: 700; + font-size: 0.7rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.nui-file-grid-origin-chip { + --nui-file-grid-origin-chip-radius: var(--script-nui-components-field-border-radius, 0px); + display: inline-flex; + align-items: center; + gap: 6px; + height: 32px; + padding: 0 9px; + box-sizing: border-box; + flex: none; + margin-right: -5px; + border-color: var(--nui-file-grid-border-color); + border-right: none; + border-radius: var(--nui-file-grid-origin-chip-radius) 0 0 var(--nui-file-grid-origin-chip-radius); + background-color: var(--nui-file-grid-filter-background); + + &+input { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + +.nui-file-grid-origin-card { + /* A long host wraps instead of widening Places, which would make the sides uneven */ + contain: inline-size; + display: flex; + flex-direction: column; + gap: 3px; + margin: 2px 8px 6px; + padding: 7px 8px; +} + +.nui-file-grid-origin-card-kind { + display: flex; + align-items: center; + gap: 6px; +} + +.nui-file-grid-origin-host { + font-family: ui-monospace, "DejaVu Sans Mono", Menlo, Consolas, monospace; + font-size: 11px; + line-height: 1.3; + white-space: normal; + overflow-wrap: anywhere; + color: var(--nui-file-grid-color, var(--color)); +} + .nui-file-grid-head { height: auto; display: flex; diff --git a/static/assets/languages/de_DE.yaml b/static/assets/languages/de_DE.yaml index 1f849406..b62342cf 100644 --- a/static/assets/languages/de_DE.yaml +++ b/static/assets/languages/de_DE.yaml @@ -77,6 +77,9 @@ fileExplorer: devices: "Geräte" drives: "Laufwerke" root: "Wurzelverzeichnis" + origin: + local: "Lokal" + remote: "Remote" errors: dropOnSameSide: "Das Verschieben von Elementen per Drag and Drop innerhalb derselben Seite wird nicht unterstützt." dropDataUnreadable: "Die Drag-and-Drop-Daten konnten nicht gelesen werden: {}" diff --git a/static/assets/languages/en_US.yaml b/static/assets/languages/en_US.yaml index 1917bde5..6fc87cf4 100644 --- a/static/assets/languages/en_US.yaml +++ b/static/assets/languages/en_US.yaml @@ -77,6 +77,9 @@ fileExplorer: devices: "Devices" drives: "Drives" root: "Root" + origin: + local: "Local" + remote: "Remote" errors: dropOnSameSide: "Moving items by drag and drop within the same side is not supported." dropDataUnreadable: "Failed to read the drag and drop data: {}" diff --git a/static/styles/main.css b/static/styles/main.css index a7ecea45..73cf7138 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -194,6 +194,8 @@ --nui-file-grid-tile-hover-background: rgba(255, 255, 255, 0.16); --nui-file-grid-tile-hover-border: rgba(255, 255, 255, 0.35); + --nui-file-grid-origin-accent: var(--theme-color); + /* Loading placeholder — flat grayscale plate with slight transparency (dark default). */ --nui-file-grid-loading-placeholder-background: rgba(255, 255, 255, 0.08); --nui-file-grid-loading-placeholder-border: rgba(255, 255, 255, 0.18); @@ -245,6 +247,8 @@ --nui-file-grid-tile-hover-background: rgba(255, 255, 255, 0.75); --nui-file-grid-tile-hover-border: rgba(0, 0, 0, 0.28); + --nui-file-grid-origin-accent: color-mix(in srgb, var(--theme-color) 80%, var(--anti-brightness-mixin) var(--anti-brightness-mixin-percent)); + /* Loading placeholder (light mode). */ --nui-file-grid-loading-placeholder-background: rgba(0, 0, 0, 0.05); --nui-file-grid-loading-placeholder-border: rgba(0, 0, 0, 0.15);