Non-blocking navigation (no main-thread scan join); downloads dialog buttons + modal confirm with folder size; darker model; bigger badge

This commit is contained in:
Pixelplanet committed 2026-10-15 15:09:10 +02:00
1 parent a1e1cbff5e
commit 587f8baebf
6 files changed
+172 -39

No files matched your search

+2 -2
View File
@@ -28,7 +28,7 @@ void main() {
// with plain diffuse only (no specular/rim) so there is no shimmer, while the
// printed button labels still read.
float diff = max(dot(N, L), 0.0);
float amb = 0.05 + 0.05 * (N.y * 0.5 + 0.5);
outColor = vec4(tex.rgb * (amb + 0.40 * diff) * pc.color.rgb,
float amb = 0.03 + 0.03 * (N.y * 0.5 + 0.5);
outColor = vec4(tex.rgb * (amb + 0.30 * diff) * pc.color.rgb,
tex.a * pc.color.a);
}
+40 -15
View File
@@ -542,12 +542,15 @@ int App::run() {
// Downloads window: job list + Pause/Stop/Clear actions. Rows are
// clickable (completed files play, actions run).
std::vector<std::string> drows;
std::vector<int> dact; // 1 pause/resume, 2 stop, 3 clear, 100+i play
std::vector<int> dact; // 1 pause/resume, 2 stop, 3 clear, 100+i play
std::vector<uint8_t> dkind; // 0 text, 1 button
if (browser_.downloadActive()) {
drows.push_back(browser_.downloadPaused() ? "[ Resume ]" : "[ Pause ]");
drows.push_back(browser_.downloadPaused() ? "Resume" : "Pause");
dact.push_back(1);
drows.push_back("[ Stop ]");
dkind.push_back(1);
drows.push_back("Stop");
dact.push_back(2);
dkind.push_back(1);
}
{
const auto& jobs = browser_.downloadJobs();
@@ -563,8 +566,7 @@ int App::run() {
nm.c_str(), pct, j.bytes / 1048576.0,
j.total / 1048576.0);
} else if (j.state == 1) {
std::snprintf(buf, sizeof(buf), "%s done (trigger to play)",
nm.c_str());
std::snprintf(buf, sizeof(buf), "%s done", nm.c_str());
} else if (j.state == 3) {
std::snprintf(buf, sizeof(buf), "%s stopped", nm.c_str());
} else {
@@ -572,17 +574,21 @@ int App::run() {
}
drows.push_back(buf);
dact.push_back(j.state == 1 ? (100 + static_cast<int>(i)) : 0);
dkind.push_back(0);
}
}
drows.push_back("[ Clear finished ]");
drows.push_back("Clear finished");
dact.push_back(3);
dkind.push_back(1);
const bool dlWin = downloadsWindow_ && !browser_.isOpen() &&
!settings_.isOpen();
!settings_.isOpen() && pendingDl_.empty();
renderer_.setDownloadsModal(false);
renderer_.setDownloads(dlWin, "Downloads", drows,
browser_.downloadActive()
? browser_.downloadText()
: "Trigger: select B: close");
: "Point + Trigger to select B: close",
dkind);
if (dlWin && in.select) {
const int hov = renderer_.downloadsHovered();
if (hov >= 0 && hov < static_cast<int>(dact.size())) {
@@ -607,23 +613,41 @@ int App::run() {
}
}
// Folder-download confirmation overlay (overrides the downloads panel).
// Folder-download confirmation: a modal with the size and two buttons.
if (!pendingDl_.empty()) {
bool confirmNow = false;
if (pendingDlFresh_) {
pendingDlFresh_ = false; // ignore the press that opened it
} else if (in.togglePlay) {
browser_.startDownload(pendingDl_, pendingDlName_, true);
pendingDl_.clear();
confirmNow = true;
} else if (in.browserToggle) {
pendingDl_.clear();
}
if (!pendingDl_.empty()) {
std::vector<std::string> crows{
"Folder: " + pendingDlName_,
"Download this folder and everything in it?"};
char sz[96];
std::snprintf(sz, sizeof(sz), "Size: %.2f GB%s",
browser_.measuredSize() / 1073741824.0,
browser_.measuring() ? " (scanning...)" : "");
std::vector<std::string> crows{"Download this folder?",
pendingDlName_, sz, "Download",
"Cancel"};
std::vector<uint8_t> ckind{0, 0, 0, 2, 1};
renderer_.setDownloadsModal(true);
renderer_.setDownloads(true, "Confirm Download", crows,
"A: Download B: Cancel");
"A: Download B: Cancel", ckind);
const int hov = renderer_.downloadsHovered();
if (in.select && hov == 3) {
confirmNow = true;
} else if (in.select && hov == 4) {
pendingDl_.clear();
}
}
if (confirmNow && !pendingDl_.empty()) {
browser_.startDownload(pendingDl_, pendingDlName_, true);
pendingDl_.clear();
}
} else {
renderer_.setDownloadsModal(false);
}
// Publish the browser panel first so the pointer can raycast it.
if (browser_.isOpen()) {
@@ -793,6 +817,7 @@ int App::run() {
pendingDl_ = fe->path;
pendingDlName_ = fe->label;
pendingDlFresh_ = true;
browser_.measureFolder(fe->path); // size for the dialog
} else {
browser_.downloadFocused();
}
+27 -10
View File
@@ -1601,16 +1601,16 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex,
const float badgeY = kBarY + kBtnHalfH * 0.55f;
if (downloadsAllDone_) {
const float tickBg[4] = {0.10f, 0.65f, 0.25f, 1.0f};
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.62f,
kBtnHalfH * 0.62f, UiShape::RoundedBox, tickBg,
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.85f,
kBtnHalfH * 0.85f, UiShape::RoundedBox, tickBg,
0.50f, 0.0f, nullptr);
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.44f,
kBtnHalfH * 0.44f, UiShape::Checkmark,
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.62f,
kBtnHalfH * 0.62f, UiShape::Checkmark,
iconCol);
} else {
const float dotCol[4] = {0.30f, 0.75f, 1.0f, 1.0f};
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.34f,
kBtnHalfH * 0.34f, UiShape::Cursor, dotCol);
drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.45f,
kBtnHalfH * 0.45f, UiShape::Cursor, dotCol);
}
}
}
@@ -1860,15 +1860,32 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex,
const float rowH = kRowH;
const float panelBg[4] = {0.02f, 0.03f, 0.05f, 0.94f};
const float panelBorder[4] = {0.25f, 0.45f, 0.65f, 0.85f};
if (downloadsModal_) { // dim backdrop so a confirmation reads as a modal
const float backdrop[4] = {0.0f, 0.0f, 0.0f, 0.55f};
drawUiElement(vpPanel, 0.0f, 0.0f, panelW * 1.35f, panelTop * 1.25f,
UiShape::RoundedBox, backdrop, 0.06f, 0.0f, nullptr);
}
drawUiElement(vpPanel, 0.0f, 0.0f, panelW, panelTop, UiShape::RoundedBox,
panelBg, 0.05f, 0.015f, panelBorder);
const float hlCol[4] = {0.18f, 0.45f, 0.85f, 0.85f};
const float btnBg[4] = {0.10f, 0.14f, 0.22f, 0.95f};
const float btnBgHover[4] = {0.18f, 0.42f, 0.82f, 0.95f};
const float btnBorder[4] = {0.30f, 0.45f, 0.65f, 0.80f};
const float primBg[4] = {0.12f, 0.50f, 0.22f, 0.95f};
for (std::size_t i = 0; i < downloadsRows_.size(); ++i) {
if (static_cast<int>(i) == downloadsHovered_) {
const float ry = panelTop - 0.2f - static_cast<float>(i) * rowH;
const float ry = panelTop - 0.2f - static_cast<float>(i) * rowH;
const uint8_t kind =
(i < downloadsKinds_.size()) ? downloadsKinds_[i] : 0;
const bool hovered = (static_cast<int>(i) == downloadsHovered_);
if (kind != 0) {
const float* bg = hovered ? btnBgHover : (kind == 2 ? primBg : btnBg);
drawUiElement(vpPanel, 0.0f, ry + rowH * 0.30f, panelW - 0.06f,
rowH * 0.50f, UiShape::RoundedBox, bg, 0.25f, 0.03f,
btnBorder);
} else if (hovered) {
drawUiElement(vpPanel, 0.0f, ry + rowH * 0.28f, panelW - 0.04f,
rowH * 0.44f, UiShape::RoundedBox, hlCol, 0.20f,
0.0f, nullptr);
rowH * 0.44f, UiShape::RoundedBox, hlCol, 0.20f, 0.0f,
nullptr);
}
}
vkCmdBindPipeline(commandBuffer_, VK_PIPELINE_BIND_POINT_GRAPHICS, textPipeline_);
+7 -1
View File
@@ -119,12 +119,16 @@ public:
// shown while downloads run (and briefly after) with the browser closed.
void setDownloads(bool visible, const std::string& title,
const std::vector<std::string>& rows,
const std::string& status) {
const std::string& status,
const std::vector<uint8_t>& kinds = {}) {
downloadsVisible_ = visible;
downloadsTitle_ = title;
downloadsRows_ = rows;
downloadsStatus_ = status;
downloadsKinds_ = kinds; // 0 = item text, 1 = button, 2 = primary button
}
// Draws a stronger modal backdrop behind the panel (for confirmations).
void setDownloadsModal(bool modal) { downloadsModal_ = modal; }
// The downloads-window row under the pointer, or -1 if none.
int downloadsHovered() const { return downloadsHovered_; }
@@ -292,6 +296,8 @@ private:
std::string downloadsStatus_;
bool downloadsActive_ = false;
bool downloadsAllDone_ = false;
bool downloadsModal_ = false;
std::vector<uint8_t> downloadsKinds_;
int downloadsHovered_ = -1;
// Flat-screen placement in the app reference space (updated by recenter and
+81 -9
View File
@@ -267,6 +267,15 @@ void Browser::scanSmbInto(const std::string& uri, ScanResult& out) {
// Starts a background scan so SMB discovery (NetBIOS + mDNS, seconds) and large
// directories never freeze the render loop. poll() publishes the result.
void Browser::beginScan(const std::string& location, bool isSmb) {
if (scanning_.load()) {
// A scan is already running (possibly waiting on the SMB lock held by a
// download). Defer this request rather than joining on the main thread,
// which would freeze the UI until the download's read returns.
pendingScanLoc_ = location;
pendingScanSmb_ = isSmb;
hasPendingScan_ = true;
return;
}
if (scanThread_.joinable()) scanThread_.join(); // previous scan has finished
scanReady_.store(false);
scanning_.store(true);
@@ -360,6 +369,7 @@ const Browser::Entry* Browser::focused() const {
Browser::~Browser() {
if (scanThread_.joinable()) scanThread_.join();
if (dlThread_.joinable()) dlThread_.join();
if (measureThread_.joinable()) measureThread_.join();
}
void Browser::startDownload(const std::string& uri, const std::string& nameIn,
@@ -711,19 +721,81 @@ void Browser::clearFinished() {
status_ = "Cleared finished downloads";
}
void Browser::measureFolder(const std::string& uri) {
#ifdef PIXELVR_HAVE_SMB
if (measuring_.load()) return;
if (measureThread_.joinable()) measureThread_.join();
measuredSize_.store(0);
measuring_.store(true);
measureThread_ = std::thread([this, uri]() {
long long total = 0;
std::function<void(const std::string&)> sum = [&](const std::string& src) {
int d = -1;
{
std::lock_guard<std::mutex> smb(smbGlobalLock());
d = smbc_opendir(src.c_str());
}
if (d < 0) return;
for (;;) {
std::string nm;
int typ = 0;
{
std::lock_guard<std::mutex> smb(smbGlobalLock());
struct smbc_dirent* de = smbc_readdir(d);
if (de == nullptr) break;
nm = de->name;
typ = de->smbc_type;
}
if (nm.empty() || nm == "." || nm == "..") continue;
std::string s = src;
if (!s.empty() && s.back() != '/') s += '/';
s += nm;
if (typ == SMBC_DIR) {
sum(s);
} else {
struct stat st{};
{
std::lock_guard<std::mutex> smb(smbGlobalLock());
smbc_stat(s.c_str(), &st);
}
total += static_cast<long long>(st.st_size);
measuredSize_.store(total); // live-updating estimate
}
}
{
std::lock_guard<std::mutex> smb(smbGlobalLock());
smbc_closedir(d);
}
};
sum(uri);
measuredSize_.store(total);
measuring_.store(false);
});
#else
(void)uri;
#endif
}
Browser::Result Browser::poll(std::string& playPath) {
// Publish a finished async directory scan (SMB discovery / large dirs).
if (scanReady_.exchange(false)) {
if (scanThread_.joinable()) scanThread_.join();
std::lock_guard<std::mutex> lk(scanMutex_);
entries_ = std::move(scanResult_.entries);
title_ = std::move(scanResult_.title);
status_ = std::move(scanResult_.status);
location_ = scanResult_.location;
inSmb_ = scanResult_.inSmb;
selected_ = 0;
lastLocation_ = location_; // remember for the next open()
lastInSmb_ = inSmb_;
{
std::lock_guard<std::mutex> lk(scanMutex_);
entries_ = std::move(scanResult_.entries);
title_ = std::move(scanResult_.title);
status_ = std::move(scanResult_.status);
location_ = scanResult_.location;
inSmb_ = scanResult_.inSmb;
selected_ = 0;
lastLocation_ = location_; // remember for the next open()
lastInSmb_ = inSmb_;
}
// Run a navigation the user requested while this scan was busy.
if (hasPendingScan_) {
hasPendingScan_ = false;
beginScan(pendingScanLoc_, pendingScanSmb_);
}
}
if (dlActive_.load()) {
const long long b = dlBytes_.load();
+15 -2
View File
@@ -99,6 +99,11 @@ public:
// Removes finished/stopped/failed jobs (deleting any partial files).
void clearFinished();
// Background size probe for a folder (shown on the download confirmation).
void measureFolder(const std::string& uri);
bool measuring() const { return measuring_.load(); }
long long measuredSize() const { return measuredSize_.load(); }
~Browser();
Browser(const Browser&) = delete;
Browser& operator=(const Browser&) = delete;
@@ -114,6 +119,11 @@ private:
};
void beginScan(const std::string& location, bool isSmb); // async navigate
void runScan(std::string location, bool isSmb); // worker body
// Navigation requested while a scan was already running (deferred so the
// main thread never blocks joining a scan stuck on the SMB lock).
std::string pendingScanLoc_;
bool pendingScanSmb_ = false;
bool hasPendingScan_ = false;
void scanLocalInto(const std::string& dir, ScanResult& out);
void scanSmbInto(const std::string& uri, ScanResult& out);
void startDownloadFile(const std::string& uri, const std::string& local);
@@ -133,8 +143,7 @@ private:
// Async directory scan (SMB discovery / large dirs) off the main thread.
std::thread scanThread_;
std::atomic<bool> scanning_{false};
std::atomic<bool> scanReady_{false}; // results waiting to publish
std::mutex scanMutex_; // guards scanResult_
std::atomic<bool> scanReady_{false}; // results waiting to publish std::mutex scanMutex_; // guards scanResult_
ScanResult scanResult_;
// Async SMB download.
@@ -152,6 +161,10 @@ private:
std::atomic<bool> dlPaused_{false};
std::atomic<bool> dlAbort_{false};
std::atomic<bool> dlStopped_{false};
// Folder size probe (background).
std::thread measureThread_;
std::atomic<bool> measuring_{false};
std::atomic<long long> measuredSize_{0};
double dlSpeed_ = 0.0; // EMA bytes/sec for the status readout
long long speedBytes_ = 0;
std::chrono::steady_clock::time_point speedTime_{};