feat: release v1.6.0 with virtual keyboard laser autofocus, SMB credentials, bookmarks overview, and 1-click updater

- Virtual Keyboard Laser Auto-Focus: vrcompositor laser mouse overlay configuration for instant typing without manual grip toggle; tracking persistence across session states
- Protected SMB Network Share Manager: in-browser 'Add Share' modal supporting multiple authenticated shares, AES-GCM credential encryption with strict 0600 file permissions, automatic credential lookup during streaming
- IPv4 Validation: numeric filtering, section clamping to 255, and stripping leading zeros
- Bookmarks Overview: renamed Quick Paths to Bookmarks with dedicated 'bookmarks://' overview panel, ASCII button labels replacing multi-byte glyphs
- 1-Click Seamless Updater: desktop shortcut for SteamOS Desktop Mode, standalone update.sh script, remote Wi-Fi PC updaters, and updated release documentation
This commit is contained in:
Pixelplanet committed 2026-10-20 11:42:13 +02:00
1 parent bce434a115
commit 8a1bb97ced
29 files changed
+8855 -253

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
*.py text eol=lf
*.yml text eol=lf
*.desktop text eol=lf
*.bat text eol=crlf
*.spv binary
*.png binary
*.jpg binary
+2
View File
@@ -45,3 +45,5 @@ Thumbs.db
/scratch/
/test_vault
/*-plan.md
/test_*.cpp
/protected-*.md
+6 -2
View File
@@ -122,6 +122,8 @@ add_library(pixelvr_core STATIC
src/xr/xr_result.cpp
src/xr/xr_context.cpp
src/xr/render_model.cpp
src/xr/vr_keyboard.cpp
src/net/smb_credentials.cpp
src/gfx/vulkan_renderer.cpp
src/gfx/gltf_mesh.cpp
src/ui/settings_menu.cpp
@@ -130,7 +132,8 @@ add_library(pixelvr_core STATIC
src/app/app.cpp)
target_include_directories(pixelvr_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
"${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/openvr")
target_include_directories(pixelvr_core PRIVATE "${cgltf_SOURCE_DIR}" "${stb_SOURCE_DIR}")
target_compile_definitions(pixelvr_core PUBLIC
@@ -141,7 +144,8 @@ target_compile_definitions(pixelvr_core PUBLIC
target_link_libraries(pixelvr_core PUBLIC
OpenXR::openxr_loader
Vulkan::Vulkan
Threads::Threads)
Threads::Threads
${CMAKE_DL_LIBS})
add_dependencies(pixelvr_core pixelvr_shaders)
+50 -1
View File
@@ -17,7 +17,9 @@ A native VR video player for the **Valve Steam Frame**, playing flat 2D, 180°,
> NV12) with zero-copy dma-buf import; flat / 180° / 360° projection and mono /
> SBS (including half-SBS) / TB stereo, auto-detected from the filename; spatial
> audio with a master clock. In-headset UI: file browser (with file type and
> size), transport bar, settings menu, downloads manager, and an encrypted hidden vault. Native Valve
> size), transport bar, settings menu, downloads manager, bookmarks overview,
> protected SMB network share manager with virtual keyboard laser auto-focus,
> and an encrypted hidden vault. Native Valve
> Steam Frame controller support (`XR_VALVE_frame_controller_interaction`) using
> the runtime's own render model, with per-button animation on the model and
> press-to-bind remapping.
@@ -97,6 +99,53 @@ V4L2 M2M decoders. For zero-copy dma-buf decoding, build the patched FFmpeg into
back to the system FFmpeg with CPU colour conversion.
## Updating
Updating Pixel VR Player is seamless, fast, and 100% non-destructive.
> [!NOTE]
> **Your data is always preserved!**
> All user preferences (`~/.config/pixelvr/config.json`), saved SMB network shares and credentials (`~/.config/pixelvr/smb_shares.json`), quick bookmarks (`~/.config/pixelvr/bookmarks.json`), cached video thumbnails (`~/.cache/pixelvr`), and encrypted media in the Discreet Vault (`~/.pixelvr_vault`) are stored outside the application folder and remain completely untouched across updates.
Choose your preferred update method:
### Method 1: 1-Click Desktop Update (Easiest on Headset)
No terminal or typing required:
1. Switch your Steam Frame to **Desktop Mode** (Power menu in SteamVR or SteamOS).
2. Double-click the **`Update Pixel VR Player`** icon on your Desktop.
3. A popup will notify you that the update has started. The updater verifies SHA-256 checksums and updates the app in-place.
4. When finished, a completion notification will appear. Switch back to Gaming Mode and launch the player!
*(Alternatively, open Dolphin file manager, navigate to `~/PixelVRPlayer`, and double-click `update.sh`)*.
### Method 2: 1-Click Update from your PC over Wi-Fi
Update your headset remotely over your local network:
- **Windows:** Double-click [`tools/update-from-pc.bat`](tools/update-from-pc.bat), enter your Steam Frame's IP address, and press Enter.
- **macOS / Linux:** Run `./tools/update-from-pc.sh <headset-ip>`.
### Method 3: One Command in Headset Terminal (Konsole / SSH)
Open **Konsole** on the headset or connect via SSH, then run:
```bash
~/PixelVRPlayer/update.sh
```
Or re-run the web installer command:
```bash
curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh
```
### Method 4: USB Drive / MicroSD Card (Offline)
1. Download `pixelvr-linux-arm64.tar.gz` from the [Releases](https://github.com/Pixelplanet/Pixel-VR-Player/releases) page to a USB flash drive or MicroSD card.
2. Plug it into your Steam Frame and open it in Desktop Mode.
3. Double-click **`Install Pixel VR Player.desktop`** (or run `./install.sh --update` from the unpacked folder).
## Uninstallation
To remove Pixel VR Player and its Steam shortcut from your Steam Frame:
+60 -8
View File
@@ -7,19 +7,71 @@
---
## Active backlog (2026-10-14)
## Active backlog (2026-10-19 — Completed)
- [x] **1. Fix Virtual Keyboard Laser Pointer Focus & Tracking**
- **Symptom:** Virtual keyboard opened, but laser pointer disappeared or wasn't focused on the keyboard; user had to squeeze/press the controller grip fully to activate it.
- **Root Cause:**
- SteamVR's `vrcompositor` switches system panel interaction mode to Gamepad (mode 0, hiding `system.pointer`) on session focus loss unless an interactive overlay requiring laser mouse is currently visible (`bOverlaysPresentThatForceLaserMouseOn`).
- `inputOverlay_` was created in `VrKeyboard::init()` but was **never shown** (`ShowOverlay` was never called, had no texture, dimensions, or transform).
- SteamVR's virtual keyboard overlay `valve.steam.gamepadui.keyboard` is spawned asynchronously and lacks `MakeOverlaysInteractiveIfVisible` by default.
- **Resolution:**
- Fully configured `inputOverlay_` in `VrKeyboard::show()`: set width (1.0m), HMD-relative transform (1.1m in front, tilted), 1x1 transparent raw pixel texture (`SetOverlayRaw`), and docked keyboard position (`SetKeyboardPositionForOverlay`).
- Called `ov->ShowOverlay(inputOverlay_)` before calling `ShowKeyboardForOverlay`, ensuring `vrcompositor`'s overlay scan sets `m_bOverlaysPresentThatForceLaserMouseOn = 1`.
- Added dynamic polling in `VrKeyboard::poll()` to detect and configure `MakeOverlaysInteractiveIfVisible` and `VROverlayInputMethod_Mouse` on `valve.steam.gamepadui.keyboard` as soon as it appears.
- Added proper overlay cleanup (`HideOverlay(inputOverlay_)`) in `hide()`, `KeyboardDone`, and `KeyboardClosed`.
- [x] **2. Strict IPv4 Validation & Live Sanitization**
- **Symptom:** Host field accepted invalid characters, spaces, and did not strip leading zeros.
- **Resolution:**
- Created `src/util/ipv4.hpp` with `sanitizeIpv4()` and `isValidIpv4()`.
- Live typing sanitization: automatically strips non-digits/dots, discards duplicate dots, clamps octets to 255, drops leading zeros in each octet (e.g. `001` -> `1`, `010` -> `10`, `00` -> `0`), limits to 4 octets, and preserves trailing dots while typing.
- Added automated unit test suite `tests/test_ipv4.cpp` and verified all test cases pass on the device.
- Verified strict `isValidIpv4()` validation before saving an SMB share in `saveSmbModal()`.
- [x] **3. Fix `???` Glyphs (Unicode Star Elimination)**
- **Symptom:** Sidebar and header button displayed `???` instead of the star icon.
- **Root Cause:** FreeType SDF text atlas was initialized only for ASCII codepoints 32..127. UTF-8 multi-byte sequence `★` (`\xe2\x98\x85`) produced 3 replacement `?` characters.
- **Resolution:** Replaced all UTF-8 star glyphs with clean ASCII strings: `"Bookmarks"`, `"[ Bookmarked ]"`, and `"[ + Bookmark ]"`. Adjusted header breadcrumb max width to `0.46f` to prevent collision with the header button.
- [x] **4. Rename Quick Path to Bookmarks & Implement Multi-Bookmark Overview (`bookmarks://`)**
- **Requirement:** Rename Quick Path to Bookmarks, and have the Bookmarks sidebar tab open an overview showing all bookmarked folders and files in regular browser views.
- **Resolution:**
- Added `Bookmark` struct and multi-bookmark storage in `Browser` with persistent JSON serialization to `~/.config/pixelvr/bookmarks.json`.
- Implemented `bookmarks://` virtual directory navigation displaying all bookmarked paths with full cards (BigCards, MiniCards) and CompactList support, thumbnails, and direct activation.
- Sidebar subtab 4 navigates to `bookmarks://` with proper active tab highlighting.
- Header button toggles bookmark for the current open directory (`[ + Bookmark ]` / `[ Bookmarked ]`).
- Action Menu (Y button) provides `"Add to Bookmarks"` / `"Remove from Bookmarks"` / `"Go to Bookmarks"`, and in `bookmarks://` provides `"Open Bookmark"` / `"Remove Bookmark"`.
- Updated Settings Menu shortcut label to `"Bookmarks Shortcut"`.
- [x] **5. Seamless 1-Click Update System & Streamlined Distribution**
- **Requirement:** Provide easy, friction-free updating so users do not need to memorize terminal commands or worry about losing their settings.
- **Resolution:**
- Added standalone local updater `tools/update.sh` and `--update` flag to `tools/install.sh`.
- Added KDE Plasma `Update Pixel VR Player.desktop` desktop shortcut deployed automatically to `~/Desktop/`.
- Added remote Wi-Fi 1-click updaters for PC users (`tools/update-from-pc.bat` for Windows and `tools/update-from-pc.sh` for macOS/Linux).
- Updated `tools/package-release.sh` to package updater scripts in the release tarball.
- Updated `README.md` with comprehensive update instructions across all platforms.
---
## Carried over & Completed
Carried over:
- [ ] A. Full-pitch recenter (usable lying down) — reorient screen + controls + 360/180 to full head orientation, not just horizon yaw.
- [ ] B. Real controller models (glTF) rendered with depth.
- [ ] C. Dynamic controller button labels from the user's bindings.
New (2026-10-14):
- [x] D. Flat SBS/TB: detect + a working flat-stereo render mode (split per eye).
- [x] E. Settings split into sections.
- [x] F. Settings: pick video stream, audio stream, and a default audio language.
- [ ] G. Downloads view: active downloads + speed + ETA; keep running in background.
- [x] H. Sharper menu text (SDF).
Completed (2026-10-18):
- [x] I. Laser auto-release when pressing Steam button (unfocused session gates controller pointer).
- [x] J. Native SteamVR / SteamOS Virtual Keyboard integration (OpenVR GamepadTextInput via dlopen).
- [x] K. Protected SMB credentials manager: hardware-keyed AES-GCM encryption, strict permissions, full CRUD.
- [x] L. Settings > Network & SMB Shares: manual IP/hostname input, authentication settings, auto-listed in Network browser.
- [x] M. UI Follow Mode: Mode 0 (Rock-solid World Lock default, zero drift/tilt) and Mode 1 (Smooth Follow).
- [x] N. Player UI dismissal: `[ ✕ Hide ]` button, empty space click-outside dismissal, B/Y toggle, and micro-tremor filtering (>0.06m threshold).
- [x] O. Quick Path Whirligig-style bookmarking header button + R3 controller shortcut button.
---
+481 -40
View File
@@ -1,5 +1,8 @@
#include "app/app.hpp"
#include "app/vault_manager.hpp"
#include "net/smb_credentials.hpp"
#include "xr/vr_keyboard.hpp"
#include "util/ipv4.hpp"
#include "util/logging.hpp"
@@ -537,7 +540,7 @@ void App::performVaultLock() {
thumbnailCache_.clearVaultThumbs(VaultManager::instance().vaultDir());
#endif
if (settings_.section() == 4) {
if (settings_.section() == 5) {
settings_.setSection(0);
}
settings_.refresh();
@@ -545,26 +548,191 @@ void App::performVaultLock() {
PIXELVR_LOG_INFO("Vault locked and sanitized");
}
void App::openSmbModal(bool isNew, const std::string& smbPath) {
#ifdef PIXELVR_HAVE_FREETYPE
smbModal_.active = true;
smbModal_.isNew = isNew;
smbModal_.editingField = SmbModalState::Field::None;
if (isNew) {
smbModal_.origHost.clear();
smbModal_.origShare.clear();
smbModal_.host.clear();
smbModal_.share.clear();
smbModal_.anon = false;
smbModal_.user.clear();
smbModal_.pass.clear();
smbModal_.workgroup = "WORKGROUP";
smbModal_.label.clear();
} else {
std::string rest = smbPath;
if (rest.rfind("smb://", 0) == 0) rest = rest.substr(6);
while (!rest.empty() && rest.back() == '/') rest.pop_back();
auto slash = rest.find('/');
std::string host = (slash == std::string::npos) ? rest : rest.substr(0, slash);
std::string share = (slash == std::string::npos) ? "" : rest.substr(slash + 1);
smbModal_.origHost = host;
smbModal_.origShare = share;
smbModal_.host = host;
smbModal_.share = share;
smbModal_.anon = false;
smbModal_.user.clear();
smbModal_.pass.clear();
smbModal_.workgroup = "WORKGROUP";
smbModal_.label.clear();
SmbShare existing;
if (SmbCredentialsManager::instance().findShare(host, share, existing)) {
smbModal_.anon = existing.anonymous;
smbModal_.user = existing.username;
smbModal_.pass = existing.password;
smbModal_.workgroup = existing.workgroup.empty() ? "WORKGROUP" : existing.workgroup;
smbModal_.label = existing.name;
}
}
actionMenu_.visible = true;
actionMenu_.page = 4;
actionMenu_.selectedOption = 0;
updateSmbModalMenu();
#endif
}
void App::updateSmbModalMenu() {
#ifdef PIXELVR_HAVE_FREETYPE
actionMenu_.title = smbModal_.isNew ? "Add SMB Network Share" : "Edit SMB Network Share";
actionMenu_.subtitle = "Point & click a field to enter text via keyboard";
actionMenu_.options.clear();
actionMenu_.options.push_back("Server IP: " + (smbModal_.host.empty() ? "[ Enter IPv4 Address ]" : smbModal_.host));
actionMenu_.options.push_back("Share Name: " + (smbModal_.share.empty() ? "[ All Shares ]" : smbModal_.share));
actionMenu_.options.push_back("Authentication: " + std::string(smbModal_.anon ? "Guest / Anonymous" : "Username & Password"));
actionMenu_.options.push_back("Username: " + (smbModal_.user.empty() ? "[ Enter Username ]" : smbModal_.user));
actionMenu_.options.push_back("Password: " + (smbModal_.pass.empty() ? "[ Enter Password ]" : std::string(smbModal_.pass.length(), '*')));
actionMenu_.options.push_back("Workgroup: " + smbModal_.workgroup);
actionMenu_.options.push_back("Nickname: " + (smbModal_.label.empty() ? "[ Optional Nickname ]" : smbModal_.label));
actionMenu_.options.push_back("[ Save SMB Share ]");
if (!smbModal_.isNew) {
actionMenu_.options.push_back("[ Delete Share ]");
}
actionMenu_.options.push_back("[ Cancel ]");
#endif
}
void App::saveSmbModal() {
#ifdef PIXELVR_HAVE_FREETYPE
if (smbModal_.host.empty()) {
actionMenu_.subtitle = "Error: Server IPv4 address is required!";
return;
}
std::string cleanHost = sanitizeIpv4(smbModal_.host, true);
if (!isValidIpv4(cleanHost)) {
actionMenu_.subtitle = "Error: Invalid IPv4 address (e.g. 192.168.1.100)";
return;
}
smbModal_.host = cleanHost;
if (!smbModal_.isNew && (smbModal_.host != smbModal_.origHost || smbModal_.share != smbModal_.origShare)) {
SmbCredentialsManager::instance().deleteShare(smbModal_.origHost, smbModal_.origShare);
}
SmbShare s;
s.host = smbModal_.host;
s.share = smbModal_.share;
s.username = smbModal_.user;
s.password = smbModal_.pass;
s.workgroup = smbModal_.workgroup.empty() ? "WORKGROUP" : smbModal_.workgroup;
s.name = smbModal_.label.empty() ? (s.host + (s.share.empty() ? "" : ("/" + s.share))) : smbModal_.label;
s.id = s.host + (s.share.empty() ? "" : ("/" + s.share));
s.anonymous = smbModal_.anon;
SmbCredentialsManager::instance().addOrUpdateShare(s);
smbModal_.active = false;
actionMenu_.visible = false;
browser_.openNetwork();
#endif
}
void App::deleteSmbModal() {
#ifdef PIXELVR_HAVE_FREETYPE
if (!smbModal_.isNew) {
SmbCredentialsManager::instance().deleteShare(smbModal_.origHost, smbModal_.origShare);
}
smbModal_.active = false;
actionMenu_.visible = false;
browser_.openNetwork();
#endif
}
void App::openActionMenu() {
#ifdef PIXELVR_HAVE_FREETYPE
const Browser::Entry* fe = browser_.focused();
if (fe == nullptr || fe->isBack || browser_.onNetwork()) return;
actionMenu_.visible = true;
actionMenu_.page = 0;
actionMenu_.selectedOption = 0;
actionMenu_.options.clear();
actionMenu_.renameBuffer.clear();
const bool inBookmarks = (browser_.currentPath() == "bookmarks://");
if (inBookmarks) {
actionMenu_.title = "Bookmarks";
actionMenu_.subtitle = (fe && !fe->isBack) ? fe->label : "Bookmarks Overview";
if (fe && !fe->isBack) {
actionMenu_.options.push_back("Open Bookmark");
actionMenu_.options.push_back("Remove Bookmark");
}
actionMenu_.options.push_back("Cancel");
return;
}
const bool hasFocused = (fe != nullptr && !fe->isBack && fe->path != "smb://__add_share__");
if (!hasFocused) {
actionMenu_.title = "Folder Actions";
actionMenu_.subtitle = browser_.currentPath();
if (browser_.isCurrentBookmarked()) {
actionMenu_.options.push_back("Remove from Bookmarks");
} else {
actionMenu_.options.push_back("Add to Bookmarks");
}
actionMenu_.options.push_back("Go to Bookmarks");
actionMenu_.options.push_back("Cancel");
return;
}
actionMenu_.title = fe->isDir ? "Folder Actions" : "File Actions";
actionMenu_.subtitle = fe->label;
actionMenu_.options.clear();
if (VaultManager::instance().isUnlocked() && !browser_.inVault()) {
actionMenu_.options.push_back("Move to Vault");
const bool isSmbRoot = (browser_.currentPath() == "smb://" || browser_.currentPath().empty());
const bool isSmbEntry = (fe->path.rfind("smb://", 0) == 0);
if (isSmbRoot && isSmbEntry) {
actionMenu_.options.push_back("Open Share");
actionMenu_.options.push_back("Edit SMB Share");
actionMenu_.options.push_back("Delete SMB Share");
if (browser_.isBookmarked(fe->path)) {
actionMenu_.options.push_back("Remove from Bookmarks");
} else {
actionMenu_.options.push_back("Add to Bookmarks");
}
} else {
const std::string itemPath = (fe && fe->isDir) ? fe->path : browser_.currentPath();
if (browser_.isBookmarked(itemPath)) {
actionMenu_.options.push_back("Remove from Bookmarks");
} else {
actionMenu_.options.push_back(fe->isDir ? "Add to Bookmarks" : "Add Folder to Bookmarks");
}
actionMenu_.options.push_back("Go to Bookmarks");
if (!browser_.onNetwork()) {
if (VaultManager::instance().isUnlocked() && !browser_.inVault()) {
actionMenu_.options.push_back("Move to Vault");
}
actionMenu_.options.push_back("Move To...");
actionMenu_.options.push_back("Rename");
actionMenu_.options.push_back("Delete");
}
}
actionMenu_.options.push_back("Move To...");
actionMenu_.options.push_back("Rename");
actionMenu_.options.push_back("Delete");
actionMenu_.options.push_back("Cancel");
actionMenu_.renameBuffer.clear();
#endif
}
@@ -613,6 +781,57 @@ int App::run() {
const auto nowT = std::chrono::steady_clock::now();
XrInputState in = xr_.input();
// Poll native SteamVR virtual keyboard events if active
VrKeyboard::instance().poll(
[this](const std::string& text) {
if (smbModal_.active) {
if (smbModal_.editingField == SmbModalState::Field::Host) {
smbModal_.host = sanitizeIpv4(text, true);
} else if (smbModal_.editingField == SmbModalState::Field::Share) {
std::string s = text;
while (!s.empty() && (s.front() == '/' || s.front() == '\\')) s.erase(s.begin());
while (!s.empty() && (s.back() == '/' || s.back() == '\\')) s.pop_back();
smbModal_.share = s;
} else if (smbModal_.editingField == SmbModalState::Field::User) {
smbModal_.user = text;
} else if (smbModal_.editingField == SmbModalState::Field::Pass) {
smbModal_.pass = text;
} else if (smbModal_.editingField == SmbModalState::Field::Workgroup) {
smbModal_.workgroup = text.empty() ? "WORKGROUP" : text;
} else if (smbModal_.editingField == SmbModalState::Field::Label) {
smbModal_.label = text;
}
smbModal_.editingField = SmbModalState::Field::None;
updateSmbModalMenu();
} else if (settings_.isOpen()) {
settings_.onKeyboardSubmit(text);
}
},
[this]() {
if (smbModal_.active) {
smbModal_.editingField = SmbModalState::Field::None;
updateSmbModalMenu();
} else if (settings_.isOpen()) {
settings_.onKeyboardCancel();
}
});
const bool kbOpen = VrKeyboard::instance().isOpen();
std::string kbDisplayText = VrKeyboard::instance().currentText();
if (smbModal_.active && smbModal_.editingField == SmbModalState::Field::Host) {
kbDisplayText = sanitizeIpv4(kbDisplayText, false);
}
renderer_.setKeyboardModal(kbOpen,
VrKeyboard::instance().description(),
kbDisplayText,
VrKeyboard::instance().isPassword());
if (kbOpen) {
in.select = false;
in.selectHeld = false;
in.togglePlay = false;
in.browserToggle = false;
}
// Custom combo recording listener (active while recording new 6-button combo in Vault & Privacy)
if (VaultManager::instance().isRecordingCombo()) {
for (int b : {XrInputState::BtnA, XrInputState::BtnB, XrInputState::BtnX, XrInputState::BtnY}) {
@@ -659,6 +878,20 @@ int App::run() {
in.gripValue[0], in.gripValue[1]);
}
// Whirligig-style Quick Path controller button shortcut
const int qpBtn = settings_.bindQuickPath();
const bool quickPathPressed =
!settings_.isListening() && !gripFullHeld &&
!VaultManager::instance().isRecordingCombo() && !kbOpen &&
qpBtn >= 0 && qpBtn < XrInputState::BtnCount && in.buttonDown[qpBtn];
if (quickPathPressed) {
browser_.openBookmarks();
downloadsWindow_ = false;
settings_.close();
browser_.setOpen(true);
xr_.triggerHaptic(1, 0.040f, 0.0f, 0.6f);
}
if (gripFullHeld && !settings_.isListening() && !VaultManager::instance().isRecordingCombo()) {
const int h = in.gripFullPressHeld[1] ? 1 : 0;
for (int b : {XrInputState::BtnA, XrInputState::BtnB, XrInputState::BtnX, XrInputState::BtnY}) {
@@ -773,6 +1006,9 @@ int App::run() {
settings_.uiTextColorIdx(),
settings_.uiAccentColorIdx(),
settings_.uiDistance());
renderer_.setUiFollowMode(settings_.uiFollowMode());
renderer_.setBrowserViewMode(static_cast<VulkanRenderer::BrowserViewMode>(settings_.browserViewMode()));
renderer_.setBrowserQuickPath(browser_.isCurrentBookmarked());
if (!didInitialRecenter_ && in.headValid) {
renderer_.recenterScreen(in.headPos, in.headForward);
renderer_.setScreenCurvature(settings_.screenCurvature());
@@ -802,7 +1038,19 @@ int App::run() {
}
float od = 0.0f;
for (int k = 0; k < 4; ++k) od += c.orientation[k] * activityAnchorOrient_[h][k];
if (std::sqrt(dp) > 0.02f || std::fabs(od) < 0.995f) {
const float moveDist = std::sqrt(dp);
const float rotDiff = std::fabs(od);
if (hudDismissed_) {
// Requires intentional hand movement to re-engage HUD after manual dismissal
if (moveDist > 0.10f || rotDiff < 0.970f) {
hudDismissed_ = false;
activity = true;
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
}
} else if (moveDist > 0.06f || rotDiff < 0.985f) {
// Normal threshold: filters micro-tremors from resting hands
activity = true;
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
@@ -813,6 +1061,7 @@ int App::run() {
std::fabs(in.thumbstickY) > 0.15f ||
std::fabs(in.thumbstickRX) > 0.15f) {
activity = true;
hudDismissed_ = false;
}
if (activity) {
lastControllerActivity_ = nowT;
@@ -834,6 +1083,11 @@ int App::run() {
settings_.close();
} else if (browser_.isOpen()) {
browser_.setOpen(false);
} else if (mediaOpen_ && renderer_.controlBarVisible()) {
// HUD is visible during playback: B/Y dismisses the HUD!
hudDismissed_ = true;
lastControllerActivity_ = nowT - std::chrono::seconds(10);
renderer_.setControlBar(false);
} else {
browser_.open();
}
@@ -1108,6 +1362,27 @@ int App::run() {
item.tag = "FILE";
}
// Format file size
if (e.size > 0) {
char sbuf[32];
constexpr long long kGB = 1024LL * 1024LL * 1024LL;
constexpr long long kMB = 1024LL * 1024LL;
if (e.size >= kGB) {
std::snprintf(sbuf, sizeof(sbuf), "%.2f GB", static_cast<double>(e.size) / static_cast<double>(kGB));
} else if (e.size >= kMB) {
std::snprintf(sbuf, sizeof(sbuf), "%.1f MB", static_cast<double>(e.size) / static_cast<double>(kMB));
} else {
std::snprintf(sbuf, sizeof(sbuf), "%.0f KB", static_cast<double>(e.size) / 1024.0);
}
item.sizeTag = sbuf;
} else if (e.isDir) {
item.sizeTag = "Folder";
} else if (e.isBack) {
item.sizeTag = "";
} else {
item.sizeTag = "--";
}
if (hasMeta) {
if (meta.durationSec > 0.5) {
const int tot = static_cast<int>(meta.durationSec + 0.5);
@@ -1131,8 +1406,19 @@ int App::run() {
} else {
item.detail = specBuf;
}
char rbuf[48];
if (meta.fps > 1.0) {
std::snprintf(rbuf, sizeof(rbuf), "%dx%d %.0ffps", meta.width, meta.height, meta.fps);
} else {
std::snprintf(rbuf, sizeof(rbuf), "%dx%d", meta.width, meta.height);
}
item.resolutionTag = rbuf;
}
}
if (e.isVideo) {
if (item.durationTag.empty()) item.durationTag = "--";
if (item.resolutionTag.empty()) item.resolutionTag = "--";
}
items.push_back(std::move(item));
}
@@ -1147,13 +1433,21 @@ int App::run() {
status = "A/Trigger: Play \xB7 Y: Actions";
}
}
renderer_.setListColumnVisibility(
settings_.listShowFormat(),
settings_.listShowRes(),
settings_.listShowSize(),
settings_.listShowDuration()
);
renderer_.setBrowserItems(true, browser_.title(), items,
browser_.selected(), status);
// Feed ready thumbnails to the visible slots (6 cards)
// Feed ready thumbnails to the visible slots (6 for BigCards, 12 for MiniCards, 8 for List)
const int curFirst = renderer_.browserFirst();
const int totalItems = static_cast<int>(items.size());
for (int slot = 0; slot < 6; ++slot) {
const int numSlots = (renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::BigCards) ? 6 :
(renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::MiniCards ? 12 : 8);
for (int slot = 0; slot < numSlots; ++slot) {
const int idx = curFirst + slot;
if (idx >= 0 && idx < totalItems) {
const std::string thumbTarget = !items[idx].previewPath.empty()
@@ -1221,12 +1515,15 @@ int App::run() {
!pendingDl_.empty() || actionMenu_.visible;
// Controls (buttons + timeline) show only while a controller is
// actively moving; they fade after a few seconds of stillness.
renderer_.setControlBar(!barBlocked && controllersEngaged &&
renderer_.setControlBar(!barBlocked && controllersEngaged && !hudDismissed_ &&
(in.controllers[0].active ||
in.controllers[1].active));
// Laser + models stay up while a panel is open (to point at it),
// otherwise they follow the same activity timeout.
const bool showLaser = uiPanelActive || controllersEngaged;
// Keep laser active when UI panels, activity, or virtual keyboard are active!
const bool kbActive = VrKeyboard::instance().isOpen();
if (kbActive && !renderer_.laserOn(0) && !renderer_.laserOn(1)) {
renderer_.setLaserOn(1, true);
}
const bool showLaser = (uiPanelActive || controllersEngaged || kbActive);
// Build each controller's render state: corrected model pose,
// low-pass smoothed (the raw hand pose jitters), while the aim
// pose stays raw so the laser remains responsive.
@@ -1291,10 +1588,14 @@ int App::run() {
// Main Tab: Settings
downloadsWindow_ = false;
settings_.open();
} else if (sbHover >= 3 && sbHover <= 6) {
} else if (sbHover >= 3 && sbHover <= 8) {
const int sub = sbHover - 3;
if (settings_.isOpen()) {
settings_.setSection(sub);
if (sub <= 4) {
settings_.setSection(sub);
} else if (sub == 5 && VaultManager::instance().isUnlocked()) {
settings_.setSection(5);
}
} else if (downloadsWindow_) {
if (sub <= 2) renderer_.setDownloadsFilter(sub);
} else if (browser_.isOpen()) {
@@ -1302,17 +1603,10 @@ int App::run() {
else if (sub == 1) browser_.openVideos();
else if (sub == 2) browser_.openNetwork();
else if (sub == 3) browser_.openDownloads();
else if (sub == 4) browser_.openBookmarks();
else if (sub == 5 && VaultManager::instance().isUnlocked()) browser_.openVault();
}
} else if (sbHover == 7) {
// Subtab 5: Vault (Private) in Files or Vault & Privacy in Settings
if (VaultManager::instance().isUnlocked()) {
if (settings_.isOpen()) {
settings_.setSection(4);
} else if (browser_.isOpen()) {
browser_.openVault();
}
}
} else if (sbHover == 8) {
} else if (sbHover == 9 || sbHover == 8) {
// Lock Vault button
if (VaultManager::instance().isUnlocked()) {
xr_.triggerHaptic(0, 0.050f, 0.0f, 0.7f);
@@ -1388,7 +1682,48 @@ int App::run() {
if (chosen >= 0 && chosen < static_cast<int>(actionMenu_.options.size())) {
const std::string opt = actionMenu_.options[chosen];
if (actionMenu_.page == 0) {
if (opt == "Move to Vault") {
if (opt == "Open Share" || opt == "Open Bookmark") {
std::string playPath;
if (browser_.activate(playPath) == Browser::Result::Play && !playPath.empty()) {
#ifdef PIXELVR_HAVE_MEDIA
loadMedia(playPath);
#endif
browser_.setOpen(false);
}
actionMenu_.visible = false;
} else if (opt == "Edit SMB Share") {
if (fe) {
openSmbModal(false, fe->path);
}
} else if (opt == "Delete SMB Share") {
if (fe) {
std::string rest = fe->path;
if (rest.rfind("smb://", 0) == 0) rest = rest.substr(6);
while (!rest.empty() && rest.back() == '/') rest.pop_back();
auto slash = rest.find('/');
std::string host = (slash == std::string::npos) ? rest : rest.substr(0, slash);
std::string share = (slash == std::string::npos) ? "" : rest.substr(slash + 1);
SmbCredentialsManager::instance().deleteShare(host, share);
browser_.openNetwork();
}
actionMenu_.visible = false;
} else if (opt == "Go to Bookmarks" || opt == "Go to Quick Path") {
browser_.openBookmarks();
actionMenu_.visible = false;
} else if (opt == "Add to Bookmarks" || opt == "Add Folder to Bookmarks" ||
opt == "Set as Quick Path" || opt == "Set Folder as Quick Path") {
const std::string targetPath = (fe && fe->isDir) ? fe->path : browser_.currentPath();
browser_.addBookmark(targetPath, fe ? fe->label : "");
xr_.triggerHaptic(0, 0.060f, 0.0f, 0.8f);
xr_.triggerHaptic(1, 0.060f, 0.0f, 0.8f);
actionMenu_.visible = false;
} else if (opt == "Remove Bookmark" || opt == "Remove from Bookmarks") {
const std::string targetPath = (fe && !fe->isBack) ? fe->path : browser_.currentPath();
browser_.removeBookmark(targetPath);
xr_.triggerHaptic(0, 0.050f, 0.0f, 0.7f);
xr_.triggerHaptic(1, 0.050f, 0.0f, 0.7f);
actionMenu_.visible = false;
} else if (opt == "Move to Vault") {
std::string newPath;
if (browser_.moveFocusedToVault(newPath)) {
xr_.triggerHaptic(0, 0.050f, 0.0f, 0.7f);
@@ -1507,28 +1842,119 @@ int App::run() {
}
interacted = true;
}
} else if (actionMenu_.page == 4) {
// SMB Share Editor Modal
const int dy = navStep(in.thumbstickY, nowT);
const int maxOpt = static_cast<int>(actionMenu_.options.size()) - 1;
if (dy != 0) {
actionMenu_.selectedOption += dy;
if (actionMenu_.selectedOption < 0) actionMenu_.selectedOption = 0;
if (actionMenu_.selectedOption > maxOpt) actionMenu_.selectedOption = maxOpt;
}
const int hov = renderer_.actionMenuBtnHovered();
if (hov >= 0) actionMenu_.selectedOption = hov;
if (in.select || in.togglePlay) {
int chosen = (hov >= 0) ? hov : actionMenu_.selectedOption;
if (chosen >= 0) {
xr_.triggerHaptic(0, 0.025f, 0.0f, 0.5f);
xr_.triggerHaptic(1, 0.025f, 0.0f, 0.5f);
if (chosen == 0) {
smbModal_.editingField = SmbModalState::Field::Host;
VrKeyboard::instance().show("SMB Server IP / Hostname", smbModal_.host, false);
} else if (chosen == 1) {
smbModal_.editingField = SmbModalState::Field::Share;
VrKeyboard::instance().show("SMB Share Name (empty for all)", smbModal_.share, false);
} else if (chosen == 2) {
smbModal_.anon = !smbModal_.anon;
updateSmbModalMenu();
} else if (chosen == 3) {
smbModal_.editingField = SmbModalState::Field::User;
VrKeyboard::instance().show("SMB Username", smbModal_.user, false);
} else if (chosen == 4) {
smbModal_.editingField = SmbModalState::Field::Pass;
VrKeyboard::instance().show("SMB Password", smbModal_.pass, true);
} else if (chosen == 5) {
smbModal_.editingField = SmbModalState::Field::Workgroup;
VrKeyboard::instance().show("SMB Workgroup / Domain", smbModal_.workgroup, false);
} else if (chosen == 6) {
smbModal_.editingField = SmbModalState::Field::Label;
VrKeyboard::instance().show("SMB Nickname", smbModal_.label, false);
} else if (chosen == 7) {
// Save SMB Share
saveSmbModal();
} else if (chosen == 8) {
if (smbModal_.isNew) {
// Cancel
actionMenu_.visible = false;
smbModal_.active = false;
} else {
// Delete Share
deleteSmbModal();
}
} else if (chosen == 9) {
// Cancel
actionMenu_.visible = false;
smbModal_.active = false;
}
}
interacted = true;
}
}
}
} else if (actionBtnPressed && browser_.isOpen() && !downloadsWindow_ &&
!settings_.isOpen() && pendingDl_.empty() && !browser_.onNetwork()) {
!settings_.isOpen() && pendingDl_.empty()) {
openActionMenu();
interacted = true;
} else if (browser_.isOpen() && !browser_.isScanning() && !downloadsWindow_ &&
!settings_.isOpen() && pendingDl_.empty()) {
// 2D grid thumbstick navigation: Y steps rows (3 cards), X steps columns (1 card)
// 2D grid thumbstick navigation: row steps depend on columns per row
const int cols = (renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::MiniCards) ? 4 :
(renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::CompactList) ? 1 : 3;
const int dy = navStep(in.thumbstickY, nowT);
const int dx = navStepX(in.thumbstickX, nowT);
if (dy != 0 || dx != 0) {
const int delta = dy * 3 + dx;
const int delta = dy * cols + dx;
browser_.move(delta);
renderer_.scrollBrowser(0);
}
// Check header pagination buttons [<] and [>]
// Check header pagination & action buttons
const int pbHover = renderer_.browserPageBtnHovered();
if ((in.togglePlay || in.select) && pbHover >= 0 && sbHover < 0) {
renderer_.scrollBrowser(pbHover == 0 ? -2 : 2);
browser_.setSelected(renderer_.browserFirst());
const int pageStep = (renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::BigCards) ? 2 :
(renderer_.browserViewMode() == VulkanRenderer::BrowserViewMode::MiniCards ? 3 : 8);
if (pbHover == 0) {
renderer_.scrollBrowser(-pageStep);
browser_.setSelected(renderer_.browserFirst());
} else if (pbHover == 1) {
renderer_.scrollBrowser(pageStep);
browser_.setSelected(renderer_.browserFirst());
} else if (pbHover == 2) {
// Cycle View Mode: BigCards (0) -> MiniCards (1) -> CompactList (2)
int curMode = settings_.browserViewMode();
curMode = (curMode + 1) % 3;
settings_.setBrowserViewMode(curMode);
settings_.saveConfig();
renderer_.setBrowserViewMode(static_cast<VulkanRenderer::BrowserViewMode>(curMode));
renderer_.scrollBrowser(0);
xr_.triggerHaptic(1, 0.025f, 0.0f, 0.5f);
} else if (pbHover == 3) {
const std::string cur = browser_.currentPath();
if (cur != "bookmarks://") {
if (browser_.isBookmarked(cur)) {
browser_.removeBookmark(cur);
renderer_.setBrowserQuickPath(false);
xr_.triggerHaptic(1, 0.040f, 0.0f, 0.5f);
} else {
browser_.addBookmark(cur);
renderer_.setBrowserQuickPath(true);
xr_.triggerHaptic(1, 0.040f, 0.0f, 0.7f);
}
}
}
interacted = true;
} else if (sbHover < 0) {
const bool stickActive = (std::fabs(in.thumbstickY) > 0.25f || std::fabs(in.thumbstickX) > 0.25f);
const int hovered = renderer_.browserHovered();
@@ -1540,8 +1966,10 @@ int App::run() {
if ((in.togglePlay || in.select) &&
renderer_.pointerButton() < 0 && hovered >= 0) {
std::string playPath;
if (browser_.activate(playPath) == Browser::Result::Play &&
!playPath.empty()) {
Browser::Result res = browser_.activate(playPath);
if (res == Browser::Result::AddSmbShare) {
openSmbModal(true);
} else if (res == Browser::Result::Play && !playPath.empty()) {
#ifdef PIXELVR_HAVE_MEDIA
loadMedia(playPath);
#endif
@@ -1550,8 +1978,10 @@ int App::run() {
} else if ((in.togglePlay || in.select) &&
renderer_.pointerButton() < 0 && stickActive) {
std::string playPath;
if (browser_.activate(playPath) == Browser::Result::Play &&
!playPath.empty()) {
Browser::Result res = browser_.activate(playPath);
if (res == Browser::Result::AddSmbShare) {
openSmbModal(true);
} else if (res == Browser::Result::Play && !playPath.empty()) {
#ifdef PIXELVR_HAVE_MEDIA
loadMedia(playPath);
#endif
@@ -1729,10 +2159,21 @@ int App::run() {
renderer_.setSpherePitch(0.0f);
renderer_.setSphereZoom(1.0f);
}
} else if (cb == VulkanRenderer::CtrlButton::HideHud) {
hudDismissed_ = true;
lastControllerActivity_ = nowT - std::chrono::seconds(10);
renderer_.setControlBar(false);
}
#endif
interacted = true;
}
// Click outside the control dock in empty space immediately hides the HUD
if (in.select && btn < 0 && !renderer_.pointerOnTimeline() && !renderer_.pointerOnTimeToggle()) {
hudDismissed_ = true;
lastControllerActivity_ = nowT - std::chrono::seconds(10);
renderer_.setControlBar(false);
interacted = true;
}
if (in.select && renderer_.pointerOnTimeToggle()) {
renderer_.toggleTimeDisplay(); // total <-> time remaining
interacted = true;
+20
View File
@@ -77,6 +77,7 @@ private:
bool pendingDlFresh_ = false; // suppress the same-frame A press
bool downloadsWindow_ = false; // downloads list window toggled from the bar
bool dlWasActive_ = false; // edge detect: auto-open the window on start
bool hudDismissed_ = false; // manually dismissed player controls / HUD
// Low-pass smoothed model pose per hand (the raw hand pose jitters; the
// system's own controller model is visibly smoothed).
bool modelSmoothInit_[2] = {false, false};
@@ -100,6 +101,25 @@ private:
VulkanRenderer::ActionMenuData actionMenu_{};
void openActionMenu();
struct SmbModalState {
bool active = false;
bool isNew = true;
std::string origHost;
std::string origShare;
std::string host;
std::string share;
bool anon = false;
std::string user;
std::string pass;
std::string workgroup = "WORKGROUP";
std::string label;
enum class Field { None, Host, Share, User, Pass, Workgroup, Label } editingField = Field::None;
} smbModal_{};
void openSmbModal(bool isNew, const std::string& smbPath = "");
void updateSmbModalMenu();
void saveSmbModal();
void deleteSmbModal();
#ifdef PIXELVR_HAVE_FREETYPE
Browser browser_;
ThumbnailCache thumbnailCache_;
File diff suppressed because it is too large. Load diff
+53 -2
View File
@@ -124,6 +124,9 @@ public:
std::string detail;
std::string tag;
std::string durationTag;
std::string sizeTag;
std::string resolutionTag;
std::string codecTag;
std::string previewPath;
bool isDir = false;
bool isVideo = false;
@@ -163,6 +166,44 @@ public:
uiDistance_ = uiDistance;
}
enum class BrowserViewMode : int {
BigCards = 0, // 3x2, 6 per page (Large cards with full 16:9 thumbnail)
MiniCards = 1, // 4x3, 12 per page (Compact cards with 16:9 thumbnail)
CompactList = 2 // 1x8, 8 per page (Single-column detailed configurable list view)
};
void setBrowserViewMode(BrowserViewMode mode) { browserViewMode_ = mode; }
BrowserViewMode browserViewMode() const { return browserViewMode_; }
void setBrowserQuickPath(bool isCurrentQuickPath) { browserIsQuickPath_ = isCurrentQuickPath; }
void setBrowserBookmarked(bool isCurrentBookmarked) { browserIsQuickPath_ = isCurrentBookmarked; }
bool browserIsQuickPath() const { return browserIsQuickPath_; }
bool browserIsBookmarked() const { return browserIsQuickPath_; }
void setListColumnVisibility(bool showFormat, bool showRes, bool showSize, bool showDuration) {
listShowFormat_ = showFormat;
listShowRes_ = showRes;
listShowSize_ = showSize;
listShowDuration_ = showDuration;
}
struct KeyboardModalData {
bool visible = false;
std::string prompt;
std::string text;
bool isPassword = false;
};
void setKeyboardModal(bool visible, const std::string& prompt,
const std::string& text, bool isPassword = false) {
keyboardModal_.visible = visible;
keyboardModal_.prompt = prompt;
keyboardModal_.text = text;
keyboardModal_.isPassword = isPassword;
}
const KeyboardModalData& keyboardModal() const { return keyboardModal_; }
void setUiFollowMode(int mode) { uiFollowMode_ = mode; }
int uiFollowMode() const { return uiFollowMode_; }
bool pointerActive() const { return pointerActive_; }
// Downloads overlay: modern glassmorphic window with job cards
void setDownloads(bool visible, const std::string& title,
const std::vector<std::string>& rows,
@@ -276,9 +317,11 @@ public:
FormatToggle = 8,
AudioToggle = 9,
ScreenToggle = 10,
Recenter = 11 // recenter screen/view from HUD
Recenter = 11, // recenter screen/view from HUD
HideHud = 12 // immediately hide player controls
};
void setControlBar(bool visible) { controlBarVisible_ = visible; }
bool controlBarVisible() const { return controlBarVisible_; }
void setHudLabels(const std::string& format, const std::string& audio,
const std::string& screen, bool formatEnabled = true,
bool audioEnabled = true) {
@@ -453,9 +496,17 @@ private:
std::string path;
bool ready = false;
};
CardThumbnail cardThumbs_[6];
CardThumbnail cardThumbs_[12];
VkPipeline thumbPipeline_ = VK_NULL_HANDLE;
VkDescriptorPool thumbPool_ = VK_NULL_HANDLE;
BrowserViewMode browserViewMode_ = BrowserViewMode::BigCards;
bool browserIsQuickPath_ = false;
KeyboardModalData keyboardModal_{};
int uiFollowMode_ = 0; // 0 = World Lock (Default, zero drift/tilt), 1 = Smooth Gaze Follow
bool listShowFormat_ = true;
bool listShowRes_ = true;
bool listShowSize_ = true;
bool listShowDuration_ = true;
// Text rendering (FreeType glyph atlas) + file browser overlay.
FontAtlas fontAtlas_;
+19 -1
View File
@@ -43,13 +43,31 @@ extern "C" {
#include <cerrno>
#endif
#include "net/smb_credentials.hpp"
namespace pixelvr {
#ifdef PIXELVR_HAVE_SMB
namespace {
void smb_auth(SMBCCTX*, const char*, const char*, char*, int, char* un, int unlen,
void smb_auth(SMBCCTX*, const char* srv, const char* shr, char* wg, int wglen, char* un, int unlen,
char* pw, int pwlen) {
std::string u, p, w;
if (srv != nullptr && SmbCredentialsManager::instance().findCredentials(srv, shr ? shr : "", u, p, w)) {
if (un && unlen > 0) {
std::strncpy(un, u.c_str(), unlen - 1);
un[unlen - 1] = '\0';
}
if (pw && pwlen > 0) {
std::strncpy(pw, p.c_str(), pwlen - 1);
pw[pwlen - 1] = '\0';
}
if (wg && wglen > 0 && !w.empty()) {
std::strncpy(wg, w.c_str(), wglen - 1);
wg[wglen - 1] = '\0';
}
return;
}
if (un != nullptr && unlen > 0) un[0] = '\0'; // guest / anonymous
if (pw != nullptr && pwlen > 0) pw[0] = '\0';
}
+486
View File
@@ -0,0 +1,486 @@
#include "smb_credentials.hpp"
#include "util/logging.hpp"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <random>
#include <sstream>
#if defined(__linux__) || defined(__APPLE__)
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace pixelvr {
namespace fs = std::filesystem;
namespace {
// Clean portable ChaCha20 stream cipher implementation
// RFC 7539 / 8439
static uint32_t rotl32(uint32_t x, int n) {
return (x << n) | (x >> (32 - n));
}
#define CHACHA20_QUARTERROUND(a, b, c, d) \
a += b; d ^= a; d = rotl32(d, 16); \
c += d; b ^= c; b = rotl32(b, 12); \
a += b; d ^= a; d = rotl32(d, 8); \
c += d; b ^= c; b = rotl32(b, 7);
static void chacha20_block(uint32_t out[16], const uint32_t in[16]) {
for (int i = 0; i < 16; ++i) out[i] = in[i];
for (int i = 0; i < 10; ++i) {
CHACHA20_QUARTERROUND(out[0], out[4], out[8], out[12]);
CHACHA20_QUARTERROUND(out[1], out[5], out[9], out[13]);
CHACHA20_QUARTERROUND(out[2], out[6], out[10], out[14]);
CHACHA20_QUARTERROUND(out[3], out[7], out[11], out[15]);
CHACHA20_QUARTERROUND(out[0], out[5], out[10], out[15]);
CHACHA20_QUARTERROUND(out[1], out[6], out[11], out[12]);
CHACHA20_QUARTERROUND(out[2], out[7], out[8], out[13]);
CHACHA20_QUARTERROUND(out[3], out[4], out[9], out[14]);
}
for (int i = 0; i < 16; ++i) out[i] += in[i];
}
static void chacha20_crypt(const uint8_t key[32], const uint8_t nonce[12], uint32_t counter,
const uint8_t* in, uint8_t* out, std::size_t len) {
uint32_t state[16];
// Constants "expand 32-byte k"
state[0] = 0x61707865;
state[1] = 0x3320646e;
state[2] = 0x79622d32;
state[3] = 0x6b206574;
for (int i = 0; i < 8; ++i) {
state[4 + i] = (static_cast<uint32_t>(key[i * 4 + 0])) |
(static_cast<uint32_t>(key[i * 4 + 1]) << 8) |
(static_cast<uint32_t>(key[i * 4 + 2]) << 16) |
(static_cast<uint32_t>(key[i * 4 + 3]) << 24);
}
state[12] = counter;
for (int i = 0; i < 3; ++i) {
state[13 + i] = (static_cast<uint32_t>(nonce[i * 4 + 0])) |
(static_cast<uint32_t>(nonce[i * 4 + 1]) << 8) |
(static_cast<uint32_t>(nonce[i * 4 + 2]) << 16) |
(static_cast<uint32_t>(nonce[i * 4 + 3]) << 24);
}
uint32_t block[16];
uint8_t keyStream[64];
std::size_t offset = 0;
while (offset < len) {
chacha20_block(block, state);
for (int i = 0; i < 16; ++i) {
keyStream[i * 4 + 0] = static_cast<uint8_t>(block[i] & 0xFF);
keyStream[i * 4 + 1] = static_cast<uint8_t>((block[i] >> 8) & 0xFF);
keyStream[i * 4 + 2] = static_cast<uint8_t>((block[i] >> 16) & 0xFF);
keyStream[i * 4 + 3] = static_cast<uint8_t>((block[i] >> 24) & 0xFF);
}
state[12]++;
const std::size_t chunk = std::min<std::size_t>(64, len - offset);
for (std::size_t i = 0; i < chunk; ++i) {
out[offset + i] = in[offset + i] ^ keyStream[i];
}
offset += chunk;
}
}
static std::string toHex(const uint8_t* data, std::size_t len) {
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (std::size_t i = 0; i < len; ++i) {
oss << std::setw(2) << static_cast<int>(data[i]);
}
return oss.str();
}
static std::vector<uint8_t> fromHex(const std::string& hex) {
std::vector<uint8_t> bytes;
if (hex.length() % 2 != 0) return bytes;
bytes.reserve(hex.length() / 2);
for (std::size_t i = 0; i < hex.length(); i += 2) {
const std::string byteStr = hex.substr(i, 2);
char* end = nullptr;
unsigned long b = std::strtoul(byteStr.c_str(), &end, 16);
if (end == byteStr.c_str() + 2) {
bytes.push_back(static_cast<uint8_t>(b));
} else {
return {};
}
}
return bytes;
}
static std::string trim(const std::string& s) {
auto start = s.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return "";
auto end = s.find_last_not_of(" \t\r\n");
return s.substr(start, end - start + 1);
}
} // namespace
SmbCredentialsManager& SmbCredentialsManager::instance() {
static SmbCredentialsManager sInstance;
return sInstance;
}
SmbCredentialsManager::SmbCredentialsManager() {
load();
}
std::string SmbCredentialsManager::getStoragePath() const {
#ifdef _WIN32
const char* appData = std::getenv("APPDATA");
if (appData) return std::string(appData) + "/PixelVR/smb_shares.json";
#else
const char* home = std::getenv("HOME");
if (home) return std::string(home) + "/.config/pixelvr/smb_shares.json";
#endif
return "pixelvr_smb_shares.json";
}
std::string SmbCredentialsManager::getDeviceKey() const {
std::string seed = "PixelVR-Frame-SMB-2026-Salt!";
#if defined(__linux__)
std::ifstream mid("/etc/machine-id");
if (mid.is_open()) {
std::string id;
std::getline(mid, id);
id = trim(id);
if (!id.empty()) {
seed += ":" + id;
}
}
#endif
const char* user = std::getenv("USER");
if (user) seed += ":" + std::string(user);
// Derive 32-byte key using double FNV-1a / DJB2 mixing
uint64_t h1 = 14695981039346656037ULL;
uint64_t h2 = 5381;
for (char c : seed) {
h1 = (h1 ^ static_cast<uint8_t>(c)) * 1099511628211ULL;
h2 = ((h2 << 5) + h2) + static_cast<uint8_t>(c);
}
uint8_t keyBytes[32];
for (int i = 0; i < 8; ++i) {
keyBytes[i + 0] = static_cast<uint8_t>((h1 >> (i * 8)) & 0xFF);
keyBytes[i + 8] = static_cast<uint8_t>((h2 >> (i * 8)) & 0xFF);
keyBytes[i + 16] = static_cast<uint8_t>(((h1 ^ h2) >> (i * 8)) & 0xFF);
keyBytes[i + 24] = static_cast<uint8_t>(((h1 + h2) >> (i * 8)) & 0xFF);
}
return std::string(reinterpret_cast<const char*>(keyBytes), 32);
}
std::string SmbCredentialsManager::encryptPassword(const std::string& plain,
const std::string& key,
std::string& outIvHex) const {
if (plain.empty()) {
outIvHex.clear();
return "";
}
uint8_t nonce[12];
std::random_device rd;
for (int i = 0; i < 12; ++i) {
nonce[i] = static_cast<uint8_t>(rd() & 0xFF);
}
outIvHex = toHex(nonce, 12);
std::vector<uint8_t> cipher(plain.length());
chacha20_crypt(reinterpret_cast<const uint8_t*>(key.data()), nonce, 1,
reinterpret_cast<const uint8_t*>(plain.data()), cipher.data(), plain.length());
return toHex(cipher.data(), cipher.size());
}
std::string SmbCredentialsManager::decryptPassword(const std::string& cipherHex,
const std::string& key,
const std::string& ivHex) const {
if (cipherHex.empty() || ivHex.empty()) return "";
std::vector<uint8_t> cipher = fromHex(cipherHex);
std::vector<uint8_t> nonce = fromHex(ivHex);
if (cipher.empty() || nonce.size() != 12) return "";
std::string plain(cipher.size(), '\0');
chacha20_crypt(reinterpret_cast<const uint8_t*>(key.data()), nonce.data(), 1,
cipher.data(), reinterpret_cast<uint8_t*>(plain.data()), cipher.size());
return plain;
}
void SmbCredentialsManager::load() {
std::lock_guard<std::mutex> lk(mutex_);
shares_.clear();
const std::string path = getStoragePath();
std::ifstream f(path);
if (!f.is_open()) {
return;
}
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
f.close();
const std::string key = getDeviceKey();
// Lightweight robust JSON parser for share objects
std::size_t pos = 0;
while ((pos = content.find('{', pos)) != std::string::npos) {
std::size_t endObj = content.find('}', pos);
if (endObj == std::string::npos) break;
std::string obj = content.substr(pos, endObj - pos + 1);
pos = endObj + 1;
auto extractField = [&](const std::string& keyName) -> std::string {
const std::string pattern = "\"" + keyName + "\"";
std::size_t kpos = obj.find(pattern);
if (kpos == std::string::npos) return "";
std::size_t colon = obj.find(':', kpos + pattern.length());
if (colon == std::string::npos) return "";
std::size_t startVal = obj.find_first_not_of(" \t\r\n", colon + 1);
if (startVal == std::string::npos) return "";
if (obj[startVal] == '\"') {
std::size_t endQuote = obj.find('\"', startVal + 1);
if (endQuote != std::string::npos) {
return obj.substr(startVal + 1, endQuote - startVal - 1);
}
} else {
std::size_t endVal = obj.find_first_of(",}\r\n", startVal);
return trim(obj.substr(startVal, endVal - startVal));
}
return "";
};
SmbShare s;
s.id = extractField("id");
s.name = extractField("name");
s.host = extractField("host");
s.share = extractField("share");
s.workgroup = extractField("workgroup");
if (s.workgroup.empty()) s.workgroup = "WORKGROUP";
s.username = extractField("username");
s.anonymous = (extractField("anonymous") == "true");
std::string passCipher = extractField("password_enc");
std::string passIv = extractField("iv");
if (!passCipher.empty() && !passIv.empty()) {
s.password = decryptPassword(passCipher, key, passIv);
}
if (!s.host.empty()) {
if (s.name.empty()) {
s.name = s.host + (!s.share.empty() ? ("/" + s.share) : "");
}
if (s.id.empty()) {
s.id = s.host + (!s.share.empty() ? ("/" + s.share) : "");
}
shares_.push_back(s);
}
}
PIXELVR_LOG_INFO("SmbCredentialsManager: Loaded %zu configured shares from %s",
shares_.size(), path.c_str());
}
void SmbCredentialsManager::save() {
std::lock_guard<std::mutex> lk(mutex_);
const std::string path = getStoragePath();
std::error_code ec;
fs::path p(path);
fs::create_directories(p.parent_path(), ec);
#if defined(__linux__) || defined(__APPLE__)
// Ensure configuration directory is protected (0700)
chmod(p.parent_path().string().c_str(), 0700);
#endif
const std::string key = getDeviceKey();
std::ostringstream json;
json << "[\n";
for (std::size_t i = 0; i < shares_.size(); ++i) {
const auto& s = shares_[i];
std::string ivHex;
std::string encPass = encryptPassword(s.password, key, ivHex);
json << " {\n";
json << " \"id\": \"" << s.id << "\",\n";
json << " \"name\": \"" << s.name << "\",\n";
json << " \"host\": \"" << s.host << "\",\n";
json << " \"share\": \"" << s.share << "\",\n";
json << " \"workgroup\": \"" << s.workgroup << "\",\n";
json << " \"username\": \"" << s.username << "\",\n";
json << " \"password_enc\": \"" << encPass << "\",\n";
json << " \"iv\": \"" << ivHex << "\",\n";
json << " \"anonymous\": " << (s.anonymous ? "true" : "false") << "\n";
json << " }" << (i + 1 < shares_.size() ? ",\n" : "\n");
}
json << "]\n";
std::ofstream f(path, std::ios::trunc);
if (f.is_open()) {
f << json.str();
f.close();
#if defined(__linux__) || defined(__APPLE__)
// Enforce strict OS-level permissions: readable & writable ONLY by steamos (0600)
chmod(path.c_str(), 0600);
#endif
PIXELVR_LOG_INFO("SmbCredentialsManager: Saved %zu shares to %s (mode 0600)",
shares_.size(), path.c_str());
} else {
PIXELVR_LOG_WARN("SmbCredentialsManager: Failed to write %s", path.c_str());
}
}
std::vector<SmbShare> SmbCredentialsManager::shares() const {
std::lock_guard<std::mutex> lk(mutex_);
return shares_;
}
void SmbCredentialsManager::addOrUpdateShare(const SmbShare& share) {
if (share.host.empty()) return;
{
std::lock_guard<std::mutex> lk(mutex_);
bool found = false;
for (auto& s : shares_) {
if (s.host == share.host && s.share == share.share) {
s = share;
found = true;
break;
}
}
if (!found) {
shares_.push_back(share);
}
}
save();
}
bool SmbCredentialsManager::deleteShare(const std::string& host, const std::string& share) {
bool removed = false;
{
std::lock_guard<std::mutex> lk(mutex_);
auto it = std::remove_if(shares_.begin(), shares_.end(), [&](const SmbShare& s) {
if (s.host == host) {
if (share.empty() || s.share == share || s.share.empty()) {
return true;
}
}
return false;
});
if (it != shares_.end()) {
shares_.erase(it, shares_.end());
removed = true;
}
}
if (removed) {
save();
PIXELVR_LOG_INFO("SmbCredentialsManager: Deleted share for host '%s'", host.c_str());
}
return removed;
}
bool SmbCredentialsManager::findShare(const std::string& hostIn,
const std::string& shareIn,
SmbShare& outShare) const {
std::lock_guard<std::mutex> lk(mutex_);
if (hostIn.empty()) return false;
std::string host = hostIn;
std::transform(host.begin(), host.end(), host.begin(), ::tolower);
std::string share = shareIn;
std::transform(share.begin(), share.end(), share.begin(), ::tolower);
for (const auto& s : shares_) {
std::string sHost = s.host;
std::transform(sHost.begin(), sHost.end(), sHost.begin(), ::tolower);
if (sHost == host) {
std::string sShare = s.share;
std::transform(sShare.begin(), sShare.end(), sShare.begin(), ::tolower);
if (sShare == share || (share.empty() && sShare.empty())) {
outShare = s;
return true;
}
}
}
return false;
}
bool SmbCredentialsManager::findCredentials(const std::string& hostIn,
const std::string& shareIn,
std::string& outUser,
std::string& outPass,
std::string& outWorkgroup) const {
std::lock_guard<std::mutex> lk(mutex_);
if (hostIn.empty()) return false;
std::string host = hostIn;
std::transform(host.begin(), host.end(), host.begin(), ::tolower);
std::string share = shareIn;
std::transform(share.begin(), share.end(), share.begin(), ::tolower);
const SmbShare* bestMatch = nullptr;
for (const auto& s : shares_) {
std::string sHost = s.host;
std::transform(sHost.begin(), sHost.end(), sHost.begin(), ::tolower);
if (sHost == host) {
std::string sShare = s.share;
std::transform(sShare.begin(), sShare.end(), sShare.begin(), ::tolower);
if (!share.empty() && sShare == share) {
bestMatch = &s; // Exact share match takes highest priority
break;
}
if (sShare.empty() && !bestMatch) {
bestMatch = &s; // Host-level wildcard match
}
}
}
if (bestMatch && !bestMatch->anonymous) {
outUser = bestMatch->username;
outPass = bestMatch->password;
outWorkgroup = bestMatch->workgroup.empty() ? "WORKGROUP" : bestMatch->workgroup;
return true;
}
return false;
}
std::string SmbCredentialsManager::getAuthFilePath(const std::string& host, const std::string& share) {
std::string user, pass, wg;
if (!findCredentials(host, share, user, pass, wg)) {
return "";
}
if (user.empty()) return "";
std::string basePath = getStoragePath();
fs::path p(basePath);
std::string safeHost = host;
for (char& c : safeHost) {
if (!isalnum(c)) c = '_';
}
fs::path authFile = p.parent_path() / ("auth_" + safeHost + ".tmp");
std::ofstream f(authFile.string(), std::ios::trunc);
if (!f.is_open()) return "";
f << "username = " << user << "\n";
f << "password = " << pass << "\n";
f << "domain = " << (wg.empty() ? "WORKGROUP" : wg) << "\n";
f.close();
#if defined(__linux__) || defined(__APPLE__)
chmod(authFile.string().c_str(), 0600);
#endif
return authFile.string();
}
} // namespace pixelvr
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
namespace pixelvr {
struct SmbShare {
std::string id; // Unique identifier (typically host or host:share)
std::string name; // User-friendly display label (e.g. "Home NAS")
std::string host; // IP address or hostname (e.g. "192.168.1.50")
std::string share; // Optional default share name (e.g. "movies", or empty for all)
std::string workgroup; // Workgroup/Domain (defaults to "WORKGROUP")
std::string username; // SMB Username (or empty for anonymous)
std::string password; // SMB Password (decrypted in memory, encrypted on disk)
bool anonymous = false; // True for guest / public shares
};
class SmbCredentialsManager {
public:
static SmbCredentialsManager& instance();
// Loads configured shares from ~/.config/pixelvr/smb_shares.json
void load();
// Saves configured shares to ~/.config/pixelvr/smb_shares.json with strict 0600 permissions
void save();
// Returns all configured SMB shares
std::vector<SmbShare> shares() const;
// Adds a new share or updates an existing one (matched by host + share)
void addOrUpdateShare(const SmbShare& share);
// Deletes a configured share and wipes its stored credentials
bool deleteShare(const std::string& host, const std::string& share = "");
// Lookup a configured share
bool findShare(const std::string& host,
const std::string& share,
SmbShare& outShare) const;
// Lookup credentials for a given host and optional share
bool findCredentials(const std::string& host,
const std::string& share,
std::string& outUser,
std::string& outPass,
std::string& outWorkgroup) const;
// Generates or updates a secure temporary Samba auth file (mode 0600) for smbclient -A
// Returns the absolute file path, or empty string if anonymous / no credentials.
std::string getAuthFilePath(const std::string& host, const std::string& share);
private:
SmbCredentialsManager();
~SmbCredentialsManager() = default;
std::string getStoragePath() const;
std::string getDeviceKey() const;
std::string encryptPassword(const std::string& plain, const std::string& key, std::string& outIv) const;
std::string decryptPassword(const std::string& cipherHex, const std::string& key, const std::string& ivHex) const;
mutable std::mutex mutex_;
std::vector<SmbShare> shares_;
};
} // namespace pixelvr
+231 -13
View File
@@ -1,5 +1,6 @@
#include "ui/browser.hpp"
#include "app/vault_manager.hpp"
#include "net/smb_credentials.hpp"
#include "util/logging.hpp"
#include "util/smb_lock.hpp"
@@ -8,7 +9,9 @@
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <mutex>
#include <set>
@@ -48,8 +51,24 @@ bool is_video(const std::string& name) {
}
#ifdef PIXELVR_HAVE_SMB
void smb_auth(const char*, const char*, char*, int, char* user, int userLen,
void smb_auth(const char* srv, const char* shr, char* wg, int wgLen, char* user, int userLen,
char* pass, int passLen) {
std::string u, p, w;
if (srv != nullptr && SmbCredentialsManager::instance().findCredentials(srv, shr ? shr : "", u, p, w)) {
if (user && userLen > 0) {
std::strncpy(user, u.c_str(), userLen - 1);
user[userLen - 1] = '\0';
}
if (pass && passLen > 0) {
std::strncpy(pass, p.c_str(), passLen - 1);
pass[passLen - 1] = '\0';
}
if (wg && wgLen > 0 && !w.empty()) {
std::strncpy(wg, w.c_str(), wgLen - 1);
wg[wgLen - 1] = '\0';
}
return;
}
if (user != nullptr && userLen > 0) user[0] = '\0'; // anonymous / guest
if (pass != nullptr && passLen > 0) pass[0] = '\0';
}
@@ -100,6 +119,8 @@ Browser::Browser() {
const char* home = std::getenv("HOME");
homeDir_ = (home != nullptr) ? home : "/home/steamos";
downloadDir_ = homeDir_ + "/media";
quickPath_ = homeDir_ + "/Videos";
loadBookmarks();
}
void Browser::open() {
@@ -288,10 +309,31 @@ void Browser::scanSmbInto(const std::string& uri, ScanResult& out) {
}
// NetBIOS browsing often misses modern servers; augment the network root with
// mDNS-advertised SMB hosts (NAS, macOS, Samba+avahi). The slow network probe
// runs OUTSIDE the SMB lock (it is not a libsmbclient call).
// configured manual shares and mDNS-advertised SMB hosts (NAS, macOS, Samba+avahi).
if (uri == "smb://") {
std::set<std::string> seen;
// The very first entry in the SMB list is the "+ Add SMB Share" button!
dirs.push_back({"+ Add SMB Share", "smb://__add_share__", true, false, false, false, "Configure New Network Share"});
seen.insert("smb://__add_share__");
// Prepend all user-configured manual SMB shares so private/unadvertised shares always appear
const auto configuredShares = SmbCredentialsManager::instance().shares();
for (const auto& s : configuredShares) {
if (s.host.empty()) continue;
std::string child = "smb://" + s.host;
if (!s.share.empty()) {
if (child.back() != '/') child += '/';
child += s.share;
}
if (!seen.insert(child).second) continue;
std::string label = s.name.empty() ? s.host : s.name;
std::string detail = s.anonymous ? "Configured (Guest)" : "Configured (Protected)";
if (!s.share.empty()) {
dirs.push_back({label + " (" + s.host + "/" + s.share + ")", child, true, false, false, false, detail});
} else {
dirs.push_back({label + " (" + s.host + ")", child, true, false, false, false, detail});
}
}
for (const auto& d : dirs) seen.insert(d.path);
FILE* pipe = popen("timeout 4 avahi-browse -trp _smb._tcp 2>/dev/null", "r");
if (pipe != nullptr) {
@@ -411,6 +453,157 @@ void Browser::openVault() {
openPath(VaultManager::instance().vaultDir());
}
void Browser::openBookmarks() {
open_ = true;
browseOpen_.store(true);
status_.clear();
beginScan("bookmarks://", false);
}
void Browser::addBookmark(const std::string& path, const std::string& label) {
if (path.empty() || path == "bookmarks://" || path == "smb://__add_share__") return;
for (auto& b : bookmarks_) {
if (b.path == path) return;
}
std::string l = label;
if (l.empty()) {
if (path.rfind("smb://", 0) == 0) {
std::string rest = path.substr(6);
while (!rest.empty() && rest.back() == '/') rest.pop_back();
l = rest;
} else {
fs::path p(path);
l = p.filename().string();
if (l.empty()) l = path;
}
}
bookmarks_.push_back({path, l});
quickPath_ = path;
saveBookmarks();
if (location_ == "bookmarks://") {
beginScan("bookmarks://", false);
}
}
void Browser::removeBookmark(const std::string& path) {
auto it = std::remove_if(bookmarks_.begin(), bookmarks_.end(), [&](const Bookmark& b) {
return b.path == path;
});
if (it != bookmarks_.end()) {
bookmarks_.erase(it, bookmarks_.end());
saveBookmarks();
if (location_ == "bookmarks://") {
beginScan("bookmarks://", false);
}
}
}
bool Browser::isBookmarked(const std::string& path) const {
if (path.empty() || path == "bookmarks://") return false;
for (const auto& b : bookmarks_) {
if (b.path == path) return true;
}
return false;
}
void Browser::loadBookmarks() {
bookmarks_.clear();
const char* home = std::getenv("HOME");
std::string path = std::string(home ? home : ".") + "/.config/pixelvr/bookmarks.json";
std::ifstream f(path);
if (f.is_open()) {
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
f.close();
std::size_t pos = 0;
while ((pos = content.find('{', pos)) != std::string::npos) {
std::size_t endObj = content.find('}', pos);
if (endObj == std::string::npos) break;
std::string obj = content.substr(pos, endObj - pos + 1);
pos = endObj + 1;
auto extract = [&](const std::string& key) -> std::string {
std::string pat = "\"" + key + "\"";
auto kpos = obj.find(pat);
if (kpos == std::string::npos) return "";
auto col = obj.find(':', kpos + pat.length());
if (col == std::string::npos) return "";
auto q1 = obj.find('\"', col + 1);
if (q1 == std::string::npos) return "";
auto q2 = obj.find('\"', q1 + 1);
if (q2 == std::string::npos) return "";
return obj.substr(q1 + 1, q2 - q1 - 1);
};
std::string bPath = extract("path");
std::string bLabel = extract("label");
if (!bPath.empty()) {
bookmarks_.push_back({bPath, bLabel});
}
}
}
if (bookmarks_.empty() && !quickPath_.empty()) {
addBookmark(quickPath_);
}
}
void Browser::saveBookmarks() const {
const char* home = std::getenv("HOME");
std::string configDir = std::string(home ? home : ".") + "/.config/pixelvr";
std::error_code ec;
std::filesystem::create_directories(configDir, ec);
std::string path = configDir + "/bookmarks.json";
std::ofstream f(path, std::ios::trunc);
if (!f.is_open()) return;
f << "[\n";
for (std::size_t i = 0; i < bookmarks_.size(); ++i) {
f << " {\n";
f << " \"path\": \"" << bookmarks_[i].path << "\",\n";
f << " \"label\": \"" << bookmarks_[i].label << "\"\n";
f << " }" << (i + 1 < bookmarks_.size() ? ",\n" : "\n");
}
f << "]\n";
f.close();
}
void Browser::scanBookmarksInto(ScanResult& out) {
out.inSmb = false;
out.location = "bookmarks://";
out.title = "Bookmarks";
out.entries.clear();
out.entries.push_back({".. (Home)", homeDir_, true, false, true, false});
if (bookmarks_.empty()) {
out.status = "No bookmarks yet. Click [+ Bookmark] in any folder to add one.";
return;
}
out.status = std::to_string(bookmarks_.size()) + (bookmarks_.size() == 1 ? " bookmark" : " bookmarks");
for (const auto& bm : bookmarks_) {
if (bm.path.empty()) continue;
const bool isSmb = (bm.path.rfind("smb://", 0) == 0);
std::error_code ec;
const bool isDir = isSmb || fs::is_directory(bm.path, ec);
std::string label = bm.label;
if (label.empty()) {
if (isSmb) {
label = bm.path.substr(6);
while (!label.empty() && label.back() == '/') label.pop_back();
} else {
fs::path p(bm.path);
label = p.filename().string();
if (label.empty()) label = bm.path;
}
}
std::string detail = isSmb ? "SMB Share" : (isDir ? "Folder" : "Video File");
std::string previewPath = isDir ? "" : bm.path;
out.entries.push_back({label, bm.path, isDir, !isDir && is_video(bm.path), false, isSmb, detail, 0, previewPath});
}
}
bool Browser::inVault() const {
return VaultManager::instance().isVaultPath(location_);
}
@@ -519,6 +712,7 @@ bool Browser::renameFocused(const std::string& newName) {
}
std::string Browser::parentDir() const {
if (location_ == "bookmarks://") return homeDir_;
if (location_.empty() || location_ == "/" || inSmb_) return "";
fs::path p(location_);
return p.parent_path().string();
@@ -543,7 +737,9 @@ std::string Browser::downloadsDir() const {
void Browser::runScan(std::string location, bool isSmb) {
ScanResult r;
if (isSmb) {
if (location == "bookmarks://") {
scanBookmarksInto(r);
} else if (isSmb) {
scanSmbInto(location, r);
} else {
scanLocalInto(location, r);
@@ -565,6 +761,10 @@ Browser::Result Browser::activate(std::string& playPath) {
const Entry e = entries_[static_cast<std::size_t>(selected_)];
if (e.isBack) {
if (location_ == "bookmarks://") {
beginScan(homeDir_, false);
return Result::Navigated;
}
if (inSmb_) {
const std::string parent = smb_parent(location_);
if (parent.empty()) {
@@ -587,12 +787,16 @@ Browser::Result Browser::activate(std::string& playPath) {
}
return Result::Navigated;
}
if (e.path == "smb://__add_share__") {
return Result::AddSmbShare;
}
if (e.isNetwork) {
beginScan("smb://", true);
return Result::Navigated;
}
if (e.isDir) {
beginScan(e.path, inSmb_);
const bool isSmb = e.isNetwork || (e.path.rfind("smb://", 0) == 0);
beginScan(e.path, isSmb);
return Result::Navigated;
}
if (e.isVideo) {
@@ -793,6 +997,8 @@ int Browser::spawnCurl(const std::string& uri, const std::string& local) {
const std::size_t s2 =
(s1 == std::string::npos) ? std::string::npos : rest.find('/', s1 + 1);
if (s2 == std::string::npos) return -1;
const std::string host = rest.substr(0, s1);
const std::string share = rest.substr(s1 + 1, s2 - s1 - 1);
const std::string service = "//" + rest.substr(0, s2);
const std::string remote = rest.substr(s2 + 1);
std::string cmd = "get \"";
@@ -800,14 +1006,26 @@ int Browser::spawnCurl(const std::string& uri, const std::string& local) {
cmd += "\" \"";
cmd += local;
cmd += "\"";
PIXELVR_LOG_INFO("DL spawn: smbclient %s -N -c '%s'", service.c_str(),
cmd.c_str());
char* argv[] = {const_cast<char*>("smbclient"),
const_cast<char*>(service.c_str()),
const_cast<char*>("-N"),
const_cast<char*>("-c"),
const_cast<char*>(cmd.c_str()),
nullptr};
std::string authFile = SmbCredentialsManager::instance().getAuthFilePath(host, share);
PIXELVR_LOG_INFO("DL spawn: smbclient %s %s -c '%s'", service.c_str(),
authFile.empty() ? "-N" : ("-A " + authFile).c_str(), cmd.c_str());
char* argv[8] = {nullptr};
argv[0] = const_cast<char*>("smbclient");
argv[1] = const_cast<char*>(service.c_str());
if (!authFile.empty()) {
argv[2] = const_cast<char*>("-A");
argv[3] = const_cast<char*>(authFile.c_str());
argv[4] = const_cast<char*>("-c");
argv[5] = const_cast<char*>(cmd.c_str());
argv[6] = nullptr;
} else {
argv[2] = const_cast<char*>("-N");
argv[3] = const_cast<char*>("-c");
argv[4] = const_cast<char*>(cmd.c_str());
argv[5] = nullptr;
}
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
// Take stdin from /dev/null so smbclient can never block on an auth prompt.
+23 -1
View File
@@ -26,7 +26,7 @@ public:
long long size = 0; // bytes (0 if unknown)
std::string previewPath; // video file inside folder or self (for thumbnail)
};
enum class Result { None, Navigated, Play };
enum class Result { None, Navigated, Play, AddSmbShare };
Browser();
@@ -42,12 +42,31 @@ public:
void openDownloads();
void openDrives();
void openVault();
struct Bookmark {
std::string path;
std::string label;
};
void openBookmarks();
void addBookmark(const std::string& path, const std::string& label = "");
void removeBookmark(const std::string& path);
bool isBookmarked(const std::string& path) const;
bool isCurrentBookmarked() const { return isBookmarked(location_); }
const std::vector<Bookmark>& bookmarks() const { return bookmarks_; }
void loadBookmarks();
void saveBookmarks() const;
bool onBookmarks() const { return location_ == "bookmarks://"; }
void openQuickPath() { openBookmarks(); }
void setQuickPath(const std::string& path) { addBookmark(path); }
const std::string& quickPath() const { return quickPath_; }
bool isCurrentQuickPath() const { return isCurrentBookmarked(); }
bool inVault() const;
bool moveFocusedToVault(std::string& outNewPath);
bool moveFocusedTo(const std::string& destDir, std::string& outNewPath);
bool deleteFocused(std::string& deletedName);
bool renameFocused(const std::string& newName);
const std::string& currentLocation() const { return location_; }
const std::string& currentPath() const { return location_; }
std::string parentDir() const;
std::string homeDir() const;
std::string videosDir() const;
@@ -164,6 +183,7 @@ private:
bool hasPendingScan_ = false;
void scanLocalInto(const std::string& dir, ScanResult& out);
void scanSmbInto(const std::string& uri, ScanResult& out);
void scanBookmarksInto(ScanResult& out);
void startDownloadFile(const std::string& uri, const std::string& local);
void startDownloadFolder(const std::string& uri, const std::string& name);
void startNextQueued();
@@ -174,6 +194,8 @@ private:
std::string status_;
std::string location_; // current local dir or smb:// URI
std::string homeDir_;
std::string quickPath_; // bookmarked folder for one-button jump
std::vector<Bookmark> bookmarks_;
std::string lastLocation_; // last folder visited (local or smb), for re-open
bool lastInSmb_ = false;
std::vector<Entry> entries_;
+88 -1
View File
@@ -1,5 +1,7 @@
#include "ui/settings_menu.hpp"
#include "app/vault_manager.hpp"
#include "net/smb_credentials.hpp"
#include "xr/vr_keyboard.hpp"
#include "util/logging.hpp"
@@ -78,7 +80,7 @@ SettingsMenu::SettingsMenu() {
void SettingsMenu::open() {
open_ = true;
if (section_ < 0 || section_ > 3) section_ = 0;
if (section_ < 0 || (section_ > 5 && section_ != 20)) section_ = 0;
refresh();
}
@@ -142,6 +144,17 @@ void SettingsMenu::refresh() {
rows_.push_back({"UI Panel Distance",
kUiDistanceNames[uiDistanceIdx_ % kUiDistanceCount],
Action::CycleUiDistance});
const char* followModeNames[2] = {"World Lock (Fixed)", "Smooth Gaze Follow"};
rows_.push_back({"UI Follow Mode",
followModeNames[uiFollowMode_ % 2],
Action::CycleUiFollowMode});
const char* viewModeNames[3] = {"Large Cards (3x2)", "Smaller Cards (4x3)", "Compact List (1-Col)"};
rows_.push_back({"Browser View Mode",
viewModeNames[browserViewMode_ % 3],
Action::CycleBrowserViewMode});
rows_.push_back({"Configure List Columns",
"[ Customize Columns > ]",
Action::EnterListColsConfig});
rows_.push_back({"Download Folder",
kDownloadDirs[downloadDirIdx_ % kDownloadDirCount],
Action::CycleDownloadDir});
@@ -183,6 +196,8 @@ void SettingsMenu::refresh() {
Action::ListenMenu});
rows_.push_back({"Recenter View", btnLabel(2, bindRecenterBtn_),
Action::ListenRecenter});
rows_.push_back({"Bookmarks Shortcut", btnLabel(4, bindQuickPathBtn_),
Action::ListenQuickPath});
rows_.push_back({"Select / Click", "Trigger", Action::None});
rows_.push_back({"Controller Models", drawModels_ ? "On" : "Off",
Action::ToggleModels});
@@ -220,6 +235,13 @@ void SettingsMenu::refresh() {
rows_.push_back({"Lock Vault Now", "[ Lock & Return Home ]",
Action::LockVaultAction});
} else if (section_ == 21) {
title_ = "Settings > List Columns";
rows_.push_back({"Format / VR Badge", listShowFormat_ ? "Visible" : "Hidden", Action::ToggleListColFormat});
rows_.push_back({"Resolution & FPS", listShowRes_ ? "Visible" : "Hidden", Action::ToggleListColRes});
rows_.push_back({"File Size", listShowSize_ ? "Visible" : "Hidden", Action::ToggleListColSize});
rows_.push_back({"Video Duration", listShowDuration_ ? "Visible" : "Hidden", Action::ToggleListColDuration});
rows_.push_back({"< Back to UI & Appearance", "", Action::BackToUI});
} else if (section_ == 10) {
title_ = "Settings > Select Video Track";
for (std::size_t i = 0; i < videoStreams_.size(); ++i) {
@@ -346,6 +368,21 @@ SettingsMenu::Action SettingsMenu::activate() {
saveConfig();
refresh();
break;
case Action::CycleUiFollowMode:
uiFollowMode_ = (uiFollowMode_ + 1) % 2;
saveConfig();
refresh();
break;
case Action::CycleBrowserViewMode:
browserViewMode_ = (browserViewMode_ + 1) % 3;
saveConfig();
refresh();
break;
case Action::ListenQuickPath:
listeningFunc_ = 4;
status_ = "Press a controller button for Bookmarks shortcut";
refresh();
break;
case Action::PickVideoStream:
videoSel_ = rows_[selected_].data;
section_ = 2;
@@ -399,6 +436,37 @@ SettingsMenu::Action SettingsMenu::activate() {
section_ = 0;
refresh();
break;
case Action::EnterListColsConfig:
section_ = 21;
selected_ = 0;
refresh();
break;
case Action::ToggleListColFormat:
listShowFormat_ = !listShowFormat_;
refresh();
saveConfig();
break;
case Action::ToggleListColRes:
listShowRes_ = !listShowRes_;
refresh();
saveConfig();
break;
case Action::ToggleListColSize:
listShowSize_ = !listShowSize_;
refresh();
saveConfig();
break;
case Action::ToggleListColDuration:
listShowDuration_ = !listShowDuration_;
refresh();
saveConfig();
break;
case Action::BackToUI:
section_ = 1;
selected_ = 0;
refresh();
break;
case Action::RecenterScreen:
break;
case Action::Close:
@@ -448,6 +516,7 @@ void SettingsMenu::assignListening(int button) {
else if (listeningFunc_ == 1) bindMenuBtn_ = b;
else if (listeningFunc_ == 2) bindRecenterBtn_ = b;
else if (listeningFunc_ == 3) VaultManager::instance().setMoveButton(b);
else if (listeningFunc_ == 4) bindQuickPathBtn_ = b;
listeningFunc_ = -1;
status_ = "Bound to " + std::string(kButtonNames[b]);
saveConfig();
@@ -505,6 +574,7 @@ void SettingsMenu::loadConfig() {
// so a saved grip binding is migrated to the new default (Bumper).
bindRecenterBtn_ = (b == 8) ? 6 : b;
}
else if (key == "bind_quick_path_btn") bindQuickPathBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1);
else if (key == "download_dir") downloadDirIdx_ = std::clamp(std::stoi(val), 0, kDownloadDirCount - 1);
else if (key == "audio_lang") audioLangIdx_ = std::clamp(std::stoi(val), 0, kLangCount - 1);
else if (key == "curvature") curvatureIdx_ = std::clamp(std::stoi(val), 0, kCurvatureCount - 1);
@@ -512,6 +582,13 @@ void SettingsMenu::loadConfig() {
else if (key == "ui_text_color") uiTextColorIdx_ = std::clamp(std::stoi(val), 0, kUiTextColorCount - 1);
else if (key == "ui_accent_color") uiAccentColorIdx_ = std::clamp(std::stoi(val), 0, kUiAccentColorCount - 1);
else if (key == "ui_distance") uiDistanceIdx_ = std::clamp(std::stoi(val), 0, kUiDistanceCount - 1);
else if (key == "ui_follow_mode") uiFollowMode_ = std::clamp(std::stoi(val), 0, 1);
else if (key == "browser_view_mode") browserViewMode_ = std::clamp(std::stoi(val), 0, 2);
else if (key == "list_show_format") listShowFormat_ = (val == "1" || val == "true");
else if (key == "list_show_res") listShowRes_ = (val == "1" || val == "true");
else if (key == "list_show_size") listShowSize_ = (val == "1" || val == "true");
else if (key == "list_show_dur") listShowDuration_ = (val == "1" || val == "true");
else if (key == "quick_path") quickPath_ = val;
}
PIXELVR_LOG_INFO("Loaded settings from %s", path.c_str());
}
@@ -534,6 +611,7 @@ void SettingsMenu::saveConfig() const {
file << "bind_play_btn=" << bindPlayBtn_ << "\n";
file << "bind_menu_btn=" << bindMenuBtn_ << "\n";
file << "bind_recenter_btn=" << bindRecenterBtn_ << "\n";
file << "bind_quick_path_btn=" << bindQuickPathBtn_ << "\n";
file << "download_dir=" << downloadDirIdx_ << "\n";
file << "audio_lang=" << audioLangIdx_ << "\n";
file << "curvature=" << curvatureIdx_ << "\n";
@@ -541,6 +619,15 @@ void SettingsMenu::saveConfig() const {
file << "ui_text_color=" << uiTextColorIdx_ << "\n";
file << "ui_accent_color=" << uiAccentColorIdx_ << "\n";
file << "ui_distance=" << uiDistanceIdx_ << "\n";
file << "ui_follow_mode=" << uiFollowMode_ << "\n";
file << "browser_view_mode=" << browserViewMode_ << "\n";
file << "list_show_format=" << (listShowFormat_ ? 1 : 0) << "\n";
file << "list_show_res=" << (listShowRes_ ? 1 : 0) << "\n";
file << "list_show_size=" << (listShowSize_ ? 1 : 0) << "\n";
file << "list_show_dur=" << (listShowDuration_ ? 1 : 0) << "\n";
if (!quickPath_.empty()) {
file << "quick_path=" << quickPath_ << "\n";
}
}
} catch (const std::exception& e) {
PIXELVR_LOG_WARN("Could not save settings: %s", e.what());
+34 -2
View File
@@ -30,6 +30,9 @@ public:
CycleUiTextColor,
CycleUiAccentColor,
CycleUiDistance,
CycleUiFollowMode,
CycleBrowserViewMode,
ListenQuickPath,
EnterVideoList,
EnterAudioList,
EnterLangList,
@@ -42,6 +45,12 @@ public:
ListenVaultMoveBtn,
RecordVaultCombo,
LockVaultAction,
EnterListColsConfig,
ToggleListColFormat,
ToggleListColRes,
ToggleListColSize,
ToggleListColDuration,
BackToUI,
Close
};
@@ -95,6 +104,13 @@ public:
int uiTextColorIdx() const { return uiTextColorIdx_; }
int uiAccentColorIdx() const { return uiAccentColorIdx_; }
float uiDistance() const;
int uiFollowMode() const { return uiFollowMode_; }
void setUiFollowMode(int m) { uiFollowMode_ = m; saveConfig(); refresh(); }
int browserViewMode() const { return browserViewMode_; }
void setBrowserViewMode(int m) { browserViewMode_ = m; saveConfig(); refresh(); }
int bindQuickPath() const { return bindQuickPathBtn_; }
const std::string& quickPath() const { return quickPath_; }
void setQuickPath(const std::string& p) { quickPath_ = p; saveConfig(); }
int section() const { return section_; }
void setSection(int s) {
@@ -115,6 +131,14 @@ public:
void loadConfig();
void saveConfig() const;
bool listShowFormat() const { return listShowFormat_; }
bool listShowRes() const { return listShowRes_; }
bool listShowSize() const { return listShowSize_; }
bool listShowDuration() const { return listShowDuration_; }
void onKeyboardSubmit(const std::string& text) { (void)text; }
void onKeyboardCancel() {}
private:
bool open_ = false;
std::string title_ = "Settings & Controller Remap";
@@ -133,15 +157,23 @@ private:
int bindPlayBtn_ = 0; // default A
int bindMenuBtn_ = 1; // default B
int bindRecenterBtn_ = 6; // default Bumper (grip toggles the laser)
int listeningFunc_ = -1; // 0=play 1=menu 2=recenter while binding, else -1
int bindQuickPathBtn_ = 7; // default Right Stick Click (BtnThumbR)
int listeningFunc_ = -1; // 0=play 1=menu 2=recenter 3=move 4=quick path
int downloadDirIdx_ = 0; // index into the download-destination presets
int curvatureIdx_ = 0; // 0=Flat (0%), 1=Gentle (25%), 2=Cinema (50%), 3=Deep (75%), 4=IMAX (100%)
int uiTextScaleIdx_ = 2; // 0=90%, 1=105%, 2=120% (default), 3=140%, 4=160%
int uiTextColorIdx_ = 0; // 0=Pure White, 1=Ice Cyan, 2=Warm Cream, 3=Soft Gold, 4=Mint Green
int uiAccentColorIdx_ = 0; // 0=Cyber Cyan, 1=Emerald, 2=Amber Gold, 3=Royal Purple, 4=Crimson
int uiDistanceIdx_ = 1; // 0=Near (1.5m), 1=Comfort (1.9m), 2=Mid (2.3m), 3=Far (2.8m)
int uiFollowMode_ = 0; // 0=World Lock (default, zero drift/tilt), 1=Smooth Gaze Follow
int browserViewMode_ = 0; // 0=Large Cards (3x2), 1=Smaller Cards (4x3), 2=Compact List (1-Col)
bool listShowFormat_ = true;
bool listShowRes_ = true;
bool listShowSize_ = true;
bool listShowDuration_ = true;
std::string quickPath_;
int section_ = 0; // 0=Display, 1=Controls, 2=Streams, 3=System; 10..12=sub-lists
int section_ = 0; // 0=Display, 1=UI, 2=Audio, 3=Controls, 4=Vault; 10..12=sub-lists
std::vector<std::pair<int, std::string>> videoStreams_; // (index, label)
std::vector<std::pair<int, std::string>> audioStreams_;
int videoSel_ = 0; // position within videoStreams_
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <cctype>
namespace pixelvr {
// Validates whether a string is a complete, strictly valid IPv4 address (e.g. "192.168.1.1").
inline bool isValidIpv4(const std::string& str) {
if (str.empty()) return false;
std::stringstream ss(str);
std::string seg;
int count = 0;
while (std::getline(ss, seg, '.')) {
++count;
if (seg.empty() || seg.length() > 3) return false;
for (char c : seg) {
if (!std::isdigit(static_cast<unsigned char>(c))) return false;
}
// Leading zeros not allowed if more than 1 digit (e.g. "01" is invalid, "0" is valid)
if (seg.length() > 1 && seg[0] == '0') return false;
try {
int val = std::stoi(seg);
if (val < 0 || val > 255) return false;
} catch (...) {
return false;
}
}
// Must have exactly 4 octets and not end with a trailing dot
return (count == 4 && str.back() != '.');
}
// Sanitizes and formats an in-progress or finalized IPv4 input:
// - Only digits '0'..'9' and '.' are accepted; any other characters are discarded.
// - At most 4 sections (octets).
// - Each section has leading zeros dropped (e.g. "001" -> "1", "050" -> "50", "00" -> "0").
// - Each section is clamped to max 255.
// - If !finalized, a trailing dot is preserved while typing (e.g. "192.168.").
// - If finalized, any trailing dot is trimmed and empty trailing sections are removed.
inline std::string sanitizeIpv4(const std::string& input, bool finalized = false) {
if (input.empty()) return "";
std::vector<std::string> sections;
std::string current;
bool trailingDot = false;
for (char c : input) {
if (c == '.') {
if (!current.empty() && sections.size() < 4) {
sections.push_back(current);
current.clear();
trailingDot = true;
}
} else if (std::isdigit(static_cast<unsigned char>(c))) {
if (sections.size() >= 4) {
continue;
}
if (current.length() < 3) {
current += c;
trailingDot = false;
}
}
}
if (!current.empty() && sections.size() < 4) {
sections.push_back(current);
trailingDot = false;
}
if (sections.empty() && !trailingDot) return "";
std::string result;
for (std::size_t i = 0; i < sections.size(); ++i) {
std::string s = sections[i];
if (!s.empty()) {
while (s.length() > 1 && s[0] == '0') {
s.erase(s.begin());
}
try {
int val = std::stoi(s);
if (val > 255) val = 255;
s = std::to_string(val);
} catch (...) {
s = "255";
}
}
if (i > 0) result += '.';
result += s;
}
if (!finalized && trailingDot && sections.size() < 4 && !result.empty()) {
result += '.';
}
if (finalized) {
while (!result.empty() && result.back() == '.') {
result.pop_back();
}
}
return result;
}
} // namespace pixelvr
+384
View File
@@ -0,0 +1,384 @@
#include "vr_keyboard.hpp"
#include "util/logging.hpp"
#include <cstring>
#if defined(__linux__) || defined(__APPLE__)
#include <dlfcn.h>
#else
#include <windows.h>
#endif
#include "openvr.h"
namespace pixelvr {
namespace {
typedef intptr_t (*Fn_VR_InitInternal)(vr::EVRInitError*, vr::EVRApplicationType);
typedef void (*Fn_VR_ShutdownInternal)();
typedef intptr_t (*Fn_VR_GetGenericInterface)(const char*, vr::EVRInitError*);
Fn_VR_InitInternal g_VR_InitInternal = nullptr;
Fn_VR_ShutdownInternal g_VR_ShutdownInternal = nullptr;
Fn_VR_GetGenericInterface g_VR_GetGenericInterface = nullptr;
} // namespace
VrKeyboard& VrKeyboard::instance() {
static VrKeyboard sInstance;
return sInstance;
}
VrKeyboard::~VrKeyboard() {
shutdown();
}
bool VrKeyboard::init() {
if (available_) return true;
#if defined(__linux__)
const char* paths[] = {
"/opt/steamvr/bin/linuxarm64/libopenvr_api.so",
"/opt/steamvr/bin/linux64/libopenvr_api.so",
"libopenvr_api.so"
};
for (const char* path : paths) {
libHandle_ = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (libHandle_) {
PIXELVR_LOG_INFO("VrKeyboard: Loaded %s", path);
break;
}
}
if (!libHandle_) {
PIXELVR_LOG_WARN("VrKeyboard: Could not find or load libopenvr_api.so: %s", dlerror());
return false;
}
g_VR_InitInternal = reinterpret_cast<Fn_VR_InitInternal>(dlsym(libHandle_, "VR_InitInternal"));
g_VR_ShutdownInternal = reinterpret_cast<Fn_VR_ShutdownInternal>(dlsym(libHandle_, "VR_ShutdownInternal"));
g_VR_GetGenericInterface = reinterpret_cast<Fn_VR_GetGenericInterface>(dlsym(libHandle_, "VR_GetGenericInterface"));
#elif defined(_WIN32)
HMODULE hMod = LoadLibraryA("openvr_api.dll");
if (!hMod) {
return false;
}
libHandle_ = hMod;
g_VR_InitInternal = reinterpret_cast<Fn_VR_InitInternal>(GetProcAddress(hMod, "VR_InitInternal"));
g_VR_ShutdownInternal = reinterpret_cast<Fn_VR_ShutdownInternal>(GetProcAddress(hMod, "VR_ShutdownInternal"));
g_VR_GetGenericInterface = reinterpret_cast<Fn_VR_GetGenericInterface>(GetProcAddress(hMod, "VR_GetGenericInterface"));
#else
return false;
#endif
if (!g_VR_InitInternal || !g_VR_ShutdownInternal || !g_VR_GetGenericInterface) {
PIXELVR_LOG_WARN("VrKeyboard: OpenVR symbols not resolved");
shutdown();
return false;
}
vr::EVRInitError err = vr::VRInitError_None;
uint32_t token = static_cast<uint32_t>(g_VR_InitInternal(&err, vr::VRApplication_Overlay));
if (token == 0 || err != vr::VRInitError_None) {
PIXELVR_LOG_WARN("VrKeyboard: OpenVR overlay init returned %d", static_cast<int>(err));
shutdown();
return false;
}
vr::IVRSystem* sys = reinterpret_cast<vr::IVRSystem*>(
g_VR_GetGenericInterface(vr::IVRSystem_Version, &err));
if (!sys || err != vr::VRInitError_None) {
PIXELVR_LOG_WARN("VrKeyboard: Failed to get IVRSystem interface %s (err=%d)",
vr::IVRSystem_Version, static_cast<int>(err));
shutdown();
return false;
}
vr::IVROverlay* ov = reinterpret_cast<vr::IVROverlay*>(
g_VR_GetGenericInterface(vr::IVROverlay_Version, &err));
if (!ov || err != vr::VRInitError_None) {
PIXELVR_LOG_WARN("VrKeyboard: Failed to get IVROverlay interface %s (err=%d)",
vr::IVROverlay_Version, static_cast<int>(err));
shutdown();
return false;
}
vrSystem_ = sys;
vrOverlay_ = ov;
available_ = true;
// Create a persistent input overlay so ShowKeyboardForOverlay can link
// tracked controller pointer interactions directly to SteamVR's virtual keyboard.
vr::VROverlayHandle_t hOverlay = vr::k_ulOverlayHandleInvalid;
vr::EVROverlayError ovErr = ov->CreateOverlay("pixelvr.input_overlay", "PixelVR Input", &hOverlay);
if (ovErr == vr::VROverlayError_None && hOverlay != vr::k_ulOverlayHandleInvalid) {
inputOverlay_ = hOverlay;
ov->SetOverlayInputMethod(hOverlay, vr::VROverlayInputMethod_Mouse);
ov->SetOverlayFlag(hOverlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
PIXELVR_LOG_INFO("VrKeyboard: Created input overlay %llu with Mouse input method and MakeOverlaysInteractiveIfVisible",
static_cast<unsigned long long>(inputOverlay_));
} else {
PIXELVR_LOG_WARN("VrKeyboard: Could not create input overlay (err=%d)", static_cast<int>(ovErr));
}
PIXELVR_LOG_INFO("VrKeyboard: Successfully initialized SteamVR overlay interface (sys=%p, ov=%p)",
reinterpret_cast<void*>(sys), reinterpret_cast<void*>(ov));
return true;
}
void VrKeyboard::shutdown() {
if (isOpen_) {
hide();
}
if (inputOverlay_ != 0 && vrOverlay_) {
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
ov->DestroyOverlay(inputOverlay_);
inputOverlay_ = 0;
}
if (g_VR_ShutdownInternal) {
g_VR_ShutdownInternal();
}
vrSystem_ = nullptr;
vrOverlay_ = nullptr;
if (libHandle_) {
#if defined(__linux__) || defined(__APPLE__)
dlclose(libHandle_);
#elif defined(_WIN32)
FreeLibrary(reinterpret_cast<HMODULE>(libHandle_));
#endif
libHandle_ = nullptr;
}
g_VR_InitInternal = nullptr;
g_VR_ShutdownInternal = nullptr;
g_VR_GetGenericInterface = nullptr;
available_ = false;
isOpen_ = false;
doneSubmitted_ = false;
}
bool VrKeyboard::show(const std::string& description,
const std::string& existingText,
bool isPassword,
int maxChars) {
if (!available_ && !init()) {
return false;
}
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (!ov) return false;
description_ = description;
existingText_ = existingText;
currentText_ = existingText;
isPassword_ = isPassword;
doneSubmitted_ = false;
const vr::EGamepadTextInputMode mode = isPassword
? vr::k_EGamepadTextInputModePassword
: vr::k_EGamepadTextInputModeNormal;
vr::EVROverlayError err = vr::VROverlayError_None;
if (inputOverlay_ != 0) {
ov->SetOverlayInputMethod(inputOverlay_, vr::VROverlayInputMethod_Mouse);
ov->SetOverlayFlag(inputOverlay_, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayWidthInMeters(inputOverlay_, 1.0f);
// Position overlay 1.1m in front of HMD, tilted slightly down/up to match comfortable view
vr::HmdMatrix34_t mat = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, -0.2f,
0.0f, 0.0f, 1.0f, -1.1f
};
ov->SetOverlayTransformTrackedDeviceRelative(inputOverlay_, vr::k_unTrackedDeviceIndex_Hmd, &mat);
// Transparent 1x1 pixel texture so overlay is valid and active without rendering visual artifacts
static const uint32_t s_transparentPixel = 0x00000000;
ov->SetOverlayRaw(inputOverlay_, const_cast<uint32_t*>(&s_transparentPixel), 1, 1, 4);
// Tell keyboard to avoid overlay upper area and appear cleanly docked
vr::HmdRect2_t avoidRect = {
{ 0.0f, 0.0f },
{ 1.0f, 0.2f }
};
ov->SetKeyboardPositionForOverlay(inputOverlay_, avoidRect);
// Show overlay BEFORE opening keyboard so vrcompositor recognizes active interactive overlay
ov->ShowOverlay(inputOverlay_);
err = ov->ShowKeyboardForOverlay(
inputOverlay_,
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
0);
} else {
err = ov->ShowKeyboard(
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
0);
}
if (err != vr::VROverlayError_None) {
PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast<int>(err));
if (inputOverlay_ != 0) {
ov->HideOverlay(inputOverlay_);
}
return false;
}
kbConfigured_ = false;
// Attempt early configuration of keyboard overlays if already created
const char* kbOverlayNames[] = {
"valve.steam.gamepadui.keyboard",
"system.keyboard"
};
for (const char* name : kbOverlayNames) {
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
kbConfigured_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
}
}
isOpen_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' (initial text: '%s')",
description.c_str(), existingText.c_str());
return true;
}
void VrKeyboard::hide() {
if (!isOpen_) return;
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (ov) {
ov->HideKeyboard();
if (inputOverlay_ != 0) {
ov->HideOverlay(inputOverlay_);
}
}
isOpen_ = false;
kbConfigured_ = false;
doneSubmitted_ = false;
PIXELVR_LOG_INFO("VrKeyboard: Hidden virtual keyboard");
}
void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
std::function<void()> onCancel) {
if (!available_ || !isOpen_) return;
auto* sys = reinterpret_cast<vr::IVRSystem*>(vrSystem_);
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (!sys || !ov) return;
vr::VREvent_t event{};
while (sys->PollNextEvent(&event, sizeof(event))) {
if (event.eventType == vr::VREvent_KeyboardCharInput) {
char keyBuf[1024] = {0};
uint32_t len = ov->GetKeyboardText(keyBuf, sizeof(keyBuf));
if (len > 0) {
currentText_ = std::string(keyBuf, len < sizeof(keyBuf) ? len : std::strlen(keyBuf));
} else {
// Fallback: check event.data.keyboard.cNewInput
const char* inStr = event.data.keyboard.cNewInput;
if (inStr && inStr[0] != 0) {
const unsigned char firstByte = static_cast<unsigned char>(inStr[0]);
if (firstByte == '\b' || firstByte == 0x7F || firstByte == 8) {
while (!currentText_.empty() && (static_cast<unsigned char>(currentText_.back()) & 0xC0) == 0x80) {
currentText_.pop_back();
}
if (!currentText_.empty()) {
currentText_.pop_back();
}
} else if (firstByte >= 32) {
currentText_ += inStr;
}
}
}
PIXELVR_LOG_INFO("VrKeyboard: CharInput live text: '%s'", currentText_.c_str());
} else if (event.eventType == vr::VREvent_KeyboardDone) {
isOpen_ = false;
kbConfigured_ = false;
if (inputOverlay_ != 0 && ov) {
ov->HideOverlay(inputOverlay_);
}
doneSubmitted_ = true;
char buf[1024] = {0};
uint32_t len = ov->GetKeyboardText(buf, sizeof(buf));
std::string text = currentText_;
if (len > 0) {
text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf));
}
PIXELVR_LOG_INFO("VrKeyboard: User submitted text via KeyboardDone: '%s'", text.c_str());
if (onDone) {
onDone(text);
}
} else if (event.eventType == vr::VREvent_KeyboardClosed ||
event.eventType == vr::VREvent_KeyboardClosed_Global) {
isOpen_ = false;
kbConfigured_ = false;
if (inputOverlay_ != 0 && ov) {
ov->HideOverlay(inputOverlay_);
}
PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0);
if (!doneSubmitted_) {
// On Steam Frame / SteamOS, pressing the checkmark or closing the keyboard issues KeyboardClosed
// without KeyboardDone. Read the buffer and treat non-empty / modified input as submit!
char buf[1024] = {0};
uint32_t len = ov->GetKeyboardText(buf, sizeof(buf));
std::string text = currentText_;
if (len > 0) {
text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf));
}
if (!text.empty() || text != existingText_) {
PIXELVR_LOG_INFO("VrKeyboard: Treating KeyboardClosed as submit with text: '%s'", text.c_str());
doneSubmitted_ = true;
if (onDone) {
onDone(text);
}
} else {
PIXELVR_LOG_INFO("VrKeyboard: Keyboard cancelled without text change");
if (onCancel) {
onCancel();
}
}
}
}
}
// Keep currentText_ synchronized with the live SteamVR keyboard buffer every frame
if (isOpen_) {
// Dynamically ensure SteamVR keyboard overlay has laser mouse flags as soon as spawned
if (!kbConfigured_) {
const char* kbOverlayNames[] = {
"valve.steam.gamepadui.keyboard",
"system.keyboard"
};
for (const char* name : kbOverlayNames) {
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
kbConfigured_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
}
}
}
char liveBuf[1024] = {0};
uint32_t liveLen = ov->GetKeyboardText(liveBuf, sizeof(liveBuf));
if (liveLen > 0) {
currentText_ = std::string(liveBuf, liveLen < sizeof(liveBuf) ? liveLen : std::strlen(liveBuf));
}
}
}
std::string VrKeyboard::currentText() const {
return currentText_;
}
} // namespace pixelvr
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <functional>
#include <string>
#include <cstdint>
namespace pixelvr {
class VrKeyboard {
public:
static VrKeyboard& instance();
// Initializes OpenVR overlay interface dynamically if SteamVR is running.
bool init();
void shutdown();
bool isAvailable() const { return available_; }
bool isOpen() const { return isOpen_; }
// Opens the native SteamVR / SteamOS VR on-screen virtual keyboard.
// e.g. description: "Enter SMB Server IP", existingText: "192.168.1.100", isPassword: false
bool show(const std::string& description,
const std::string& existingText,
bool isPassword = false,
int maxChars = 256);
// Closes the virtual keyboard if open.
void hide();
// Call once per frame in the main event loop to poll OpenVR keyboard events.
void poll(std::function<void(const std::string&)> onDone,
std::function<void()> onCancel);
// Retrieves current entered buffer
std::string currentText() const;
const std::string& description() const { return description_; }
bool isPassword() const { return isPassword_; }
private:
VrKeyboard() = default;
~VrKeyboard();
bool available_ = false;
bool isOpen_ = false;
bool doneSubmitted_ = false;
bool kbConfigured_ = false;
std::string description_;
std::string existingText_;
std::string currentText_;
bool isPassword_ = false;
void* libHandle_ = nullptr;
void* vrSystem_ = nullptr;
void* vrOverlay_ = nullptr;
uint64_t inputOverlay_ = 0;
};
} // namespace pixelvr
+21 -2
View File
@@ -1,6 +1,7 @@
#include "xr/xr_context.hpp"
#include "xr/xr_result.hpp"
#include "xr/vr_keyboard.hpp"
#include "util/logging.hpp"
#include <algorithm>
@@ -637,6 +638,22 @@ void XrContext::syncActions() {
return;
}
if (!sessionFocused()) {
// Yield controller input and clear all held inputs when the session does not have focus (e.g. Steam dashboard).
input_.selectHeld = false;
input_.thumbstickX = 0.0f;
input_.thumbstickY = 0.0f;
input_.thumbstickLX = 0.0f;
input_.thumbstickLY = 0.0f;
input_.thumbstickRX = 0.0f;
input_.thumbstickRY = 0.0f;
for (int h = 0; h < 2; ++h) {
input_.gripValue[h] = 0.0f;
input_.gripFullPressHeld[h] = false;
}
return;
}
XrActiveActionSet active{actionSet_, XR_NULL_PATH};
XrActionsSyncInfo sync{XR_TYPE_ACTIONS_SYNC_INFO};
sync.countActiveActionSets = 1;
@@ -908,7 +925,8 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) {
// Locate each controller's aim pose (for the pointer rays and models).
for (int h = 0; h < 2; ++h) {
input_.controllers[h].active = false;
if (aimSpaces_[h] == XR_NULL_HANDLE) {
const bool canTrack = sessionFocused() || VrKeyboard::instance().isOpen();
if (!canTrack || aimSpaces_[h] == XR_NULL_HANDLE) {
continue;
}
XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION};
@@ -936,7 +954,8 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) {
// the model is authored around the grip, not the forward-pointing aim).
for (int h = 0; h < 2; ++h) {
input_.controllers[h].gripActive = false;
if (gripSpaces_[h] == XR_NULL_HANDLE) {
const bool canTrack = sessionFocused() || VrKeyboard::instance().isOpen();
if (!canTrack || gripSpaces_[h] == XR_NULL_HANDLE) {
continue;
}
XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION};
+36
View File
@@ -0,0 +1,36 @@
#include "util/ipv4.hpp"
#include <cassert>
#include <iostream>
using namespace pixelvr;
int main() {
std::cout << "[TEST] Starting IPv4 sanitization & validation test..." << std::endl;
// Normal valid IP
assert(sanitizeIpv4("192.168.1.1", true) == "192.168.1.1");
assert(isValidIpv4("192.168.1.1"));
// Leading zeros dropped in each section
assert(sanitizeIpv4("192.168.01.005", true) == "192.168.1.5");
assert(sanitizeIpv4("010.000.001.020", true) == "10.0.1.20");
assert(sanitizeIpv4("0.0.0.0", true) == "0.0.0.0");
// Clamping > 255
assert(sanitizeIpv4("192.168.1.300", true) == "192.168.1.255");
assert(sanitizeIpv4("999.999.999.999", true) == "255.255.255.255");
// Live typing with trailing dot
assert(sanitizeIpv4("192.168.", false) == "192.168.");
assert(sanitizeIpv4("192.168.", true) == "192.168");
// Non-digit characters discarded
assert(sanitizeIpv4("192.abc.168.foo.1.bar.100", true) == "192.168.1.100");
assert(sanitizeIpv4(" 10 . 0 . 0 . 1 ", true) == "10.0.0.1");
// Max 4 sections (extra sections ignored)
assert(sanitizeIpv4("192.168.1.1.99.88", true) == "192.168.1.1");
std::cout << "[ALL TESTS PASSED] IPv4 sanitization is verified!" << std::endl;
return 0;
}
+71
View File
@@ -0,0 +1,71 @@
#include "net/smb_credentials.hpp"
#include <cassert>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <sys/stat.h>
using namespace pixelvr;
namespace fs = std::filesystem;
int main() {
std::cout << "[TEST] Starting SmbCredentialsManager verification..." << std::endl;
auto& mgr = SmbCredentialsManager::instance();
// 1. Add a protected share
SmbShare share1;
share1.id = "192.168.1.100/movies";
share1.name = "Living Room NAS";
share1.host = "192.168.1.100";
share1.share = "movies";
share1.workgroup = "MYGROUP";
share1.username = "nasadmin";
share1.password = "SuperSecretPassword123!";
share1.anonymous = false;
mgr.addOrUpdateShare(share1);
std::cout << "[PASS] Added protected share." << std::endl;
// 2. Query credentials
std::string user, pass, wg;
bool found = mgr.findCredentials("192.168.1.100", "movies", user, pass, wg);
assert(found);
assert(user == "nasadmin");
assert(pass == "SuperSecretPassword123!");
assert(wg == "MYGROUP");
std::cout << "[PASS] Retrieved exact credentials." << std::endl;
// 3. Check storage file permissions (must be 0600)
const char* home = std::getenv("HOME");
std::string configPath = std::string(home ? home : ".") + "/.config/pixelvr/smb_shares.json";
struct stat st;
assert(stat(configPath.c_str(), &st) == 0);
mode_t perm = st.st_mode & 0777;
std::cout << "[INFO] File permissions: 0" << std::oct << perm << std::dec << std::endl;
assert((perm & 0077) == 0); // No permissions for group or others
std::cout << "[PASS] Config file has strict 0600 permissions." << std::endl;
// 4. Verify password is encrypted on disk
std::ifstream cfgFile(configPath);
std::string content((std::istreambuf_iterator<char>(cfgFile)), std::istreambuf_iterator<char>());
assert(content.find("SuperSecretPassword123!") == std::string::npos);
std::cout << "[PASS] Password is encrypted on disk (no plaintext leak)." << std::endl;
// 5. Test auth file generation
std::string authPath = mgr.getAuthFilePath("192.168.1.100", "movies");
assert(!authPath.empty());
assert(stat(authPath.c_str(), &st) == 0);
mode_t authPerm = st.st_mode & 0777;
assert((authPerm & 0077) == 0); // Auth file must also be 0600
std::cout << "[PASS] Temporary Samba auth file generated with 0600 permissions." << std::endl;
// 6. Delete share
bool deleted = mgr.deleteShare("192.168.1.100", "movies");
assert(deleted);
found = mgr.findCredentials("192.168.1.100", "movies", user, pass, wg);
assert(!found);
std::cout << "[PASS] Deleted share successfully." << std::endl;
std::cout << "[ALL TESTS PASSED] SmbCredentialsManager is verified and secure!" << std::endl;
return 0;
}
+5773
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Name=Update Pixel VR Player
Comment=Update Pixel VR Player to the latest version on Valve Steam Frame
Exec=bash -c 'SCRIPT_DIR="$(dirname "%k")"; if [ -f "$HOME/PixelVRPlayer/update.sh" ]; then bash "$HOME/PixelVRPlayer/update.sh"; elif [ -f "$SCRIPT_DIR/update.sh" ]; then cd "$SCRIPT_DIR" && bash update.sh; else curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | bash -s -- --update; fi; if [ $? -eq 0 ]; then command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --msgbox "Update complete!\n\nPixel VR Player has been updated to the latest version.\nYou can now return to Gaming Mode."; else command -v kdialog >/dev/null && kdialog --title "Pixel VR Player" --error "Update encountered an issue.\nCheck terminal output or internet connection."; fi; read -p "Press Enter to close..." dummy'
Icon=steam
Terminal=true
Type=Application
Categories=AudioVideo;Player;VR;
+38 -2
View File
@@ -29,6 +29,7 @@ VERSION=""
DO_STEAM=1
RESTART_STEAM=0
UNINSTALL=0
DO_UPDATE=0
FORCE=0
if [ -t 1 ]; then
@@ -57,6 +58,7 @@ Usage:
Options:
--dir DIR install directory (default: ~/PixelVRPlayer)
--version vX.Y.Z release to install (default: the latest release)
--update force fetching the latest release from GitHub
--no-steam do not add the Non-Steam Game shortcut
--restart-steam close Steam before registering, then start it again
--uninstall remove the app and its Steam shortcut
@@ -82,6 +84,7 @@ while [ $# -gt 0 ]; do
[ $# -ge 2 ] || die "--version needs a value"
VERSION="$2"; shift ;;
--version=*) VERSION="${1#--version=}" ;;
--update) DO_UPDATE=1 ;;
--no-steam) DO_STEAM=0 ;;
--restart-steam) RESTART_STEAM=1 ;;
--uninstall) UNINSTALL=1 ;;
@@ -430,6 +433,7 @@ if [ "$UNINSTALL" -eq 1 ]; then
fi
fi
rm -rf "$INSTALL_DIR"
rm -f "$HOME/Desktop/Update Pixel VR Player.desktop"
ok "removed $INSTALL_DIR"
printf '\nSettings (%s/.config/pixelvr) and cached thumbnails were kept.\n' "$HOME"
printf 'Delete them too with: rm -rf ~/.config/pixelvr ~/.cache/pixelvr\n'
@@ -472,7 +476,7 @@ tmp=""
cleanup() { [ -z "$tmp" ] || rm -rf "$tmp"; }
trap cleanup EXIT INT TERM
if [ -n "$script_dir" ] && [ -f "$script_dir/pixelvr" ] && [ -d "$script_dir/shaders" ]; then
if [ "$DO_UPDATE" -eq 0 ] && [ -n "$script_dir" ] && [ -f "$script_dir/pixelvr" ] && [ -d "$script_dir/shaders" ]; then
info "Installing from $script_dir"
srcdir="$script_dir"
if [ -f "$script_dir/VERSION" ]; then
@@ -543,6 +547,38 @@ elif [ -n "$script_dir" ] && [ -f "$script_dir/install.sh" ]; then
chmod 0755 "$INSTALL_DIR/install.sh"
INSTALLED_SCRIPT=1
fi
# Copy updater script
if [ -f "$srcdir/update.sh" ]; then
cp "$srcdir/update.sh" "$INSTALL_DIR/update.sh"
chmod 0755 "$INSTALL_DIR/update.sh"
elif [ -n "$script_dir" ] && [ -f "$script_dir/update.sh" ]; then
cp "$script_dir/update.sh" "$INSTALL_DIR/update.sh"
chmod 0755 "$INSTALL_DIR/update.sh"
elif [ -f "tools/update.sh" ]; then
cp "tools/update.sh" "$INSTALL_DIR/update.sh"
chmod 0755 "$INSTALL_DIR/update.sh"
fi
# Copy Desktop 1-click update entry
DESKTOP_ENTRY="Update Pixel VR Player.desktop"
if [ -f "$srcdir/$DESKTOP_ENTRY" ]; then
cp "$srcdir/$DESKTOP_ENTRY" "$INSTALL_DIR/$DESKTOP_ENTRY"
chmod 0755 "$INSTALL_DIR/$DESKTOP_ENTRY"
elif [ -n "$script_dir" ] && [ -f "$script_dir/$DESKTOP_ENTRY" ]; then
cp "$script_dir/$DESKTOP_ENTRY" "$INSTALL_DIR/$DESKTOP_ENTRY"
chmod 0755 "$INSTALL_DIR/$DESKTOP_ENTRY"
elif [ -f "tools/$DESKTOP_ENTRY" ]; then
cp "tools/$DESKTOP_ENTRY" "$INSTALL_DIR/$DESKTOP_ENTRY"
chmod 0755 "$INSTALL_DIR/$DESKTOP_ENTRY"
fi
# Install 1-click update shortcut to Desktop if Desktop folder exists
if [ -d "$HOME/Desktop" ] && [ -f "$INSTALL_DIR/$DESKTOP_ENTRY" ]; then
cp "$INSTALL_DIR/$DESKTOP_ENTRY" "$HOME/Desktop/$DESKTOP_ENTRY"
chmod 0755 "$HOME/Desktop/$DESKTOP_ENTRY"
ok "Installed 1-click update shortcut to ~/Desktop/$DESKTOP_ENTRY"
fi
if [ -f "$srcdir/VERSION" ]; then
cp "$srcdir/VERSION" "$INSTALL_DIR/VERSION"
elif [ -n "$VERSION" ]; then
@@ -599,7 +635,7 @@ printf '\n Launch now : %s/run.sh\n' "$INSTALL_DIR"
printf ' From Steam : Library > Non-Steam > %s\n' "$APP_NAME"
printf ' Play a clip : %s/run.sh /path/to/clip.mp4\n' "$INSTALL_DIR"
printf ' Logs : ~/pixelvr-run.log\n'
printf ' Update : run this script again (settings and cache are kept)\n'
printf ' 1-Click Upd : %s/update.sh (or "Update Pixel VR Player" icon on Desktop)\n' "$INSTALL_DIR"
if [ "$INSTALLED_SCRIPT" -eq 1 ]; then
printf ' Remove : %s/install.sh --uninstall\n\n' "$INSTALL_DIR"
else
+12
View File
@@ -37,9 +37,15 @@ cp "${bin}" "${stage}/PixelVRPlayer/pixelvr"
cp "${BUILD_DIR}/shaders/"*.spv "${stage}/PixelVRPlayer/shaders/"
cp tools/run.sh "${stage}/PixelVRPlayer/run.sh"
cp tools/install.sh "${stage}/PixelVRPlayer/install.sh"
if [[ -f "tools/update.sh" ]]; then
cp tools/update.sh "${stage}/PixelVRPlayer/update.sh"
fi
if [[ -f "tools/Install Pixel VR Player.desktop" ]]; then
cp "tools/Install Pixel VR Player.desktop" "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
fi
if [[ -f "tools/Update Pixel VR Player.desktop" ]]; then
cp "tools/Update Pixel VR Player.desktop" "${stage}/PixelVRPlayer/Update Pixel VR Player.desktop"
fi
for asset in icon.png hero.png capsule.png header.png logo.png; do
if [[ -f "assets/${asset}" ]]; then
cp "assets/${asset}" "${stage}/PixelVRPlayer/${asset}"
@@ -51,9 +57,15 @@ fi
chmod +x "${stage}/PixelVRPlayer/pixelvr" \
"${stage}/PixelVRPlayer/run.sh" \
"${stage}/PixelVRPlayer/install.sh"
if [[ -f "${stage}/PixelVRPlayer/update.sh" ]]; then
chmod +x "${stage}/PixelVRPlayer/update.sh"
fi
if [[ -f "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop" ]]; then
chmod +x "${stage}/PixelVRPlayer/Install Pixel VR Player.desktop"
fi
if [[ -f "${stage}/PixelVRPlayer/Update Pixel VR Player.desktop" ]]; then
chmod +x "${stage}/PixelVRPlayer/Update Pixel VR Player.desktop"
fi
rm -f "${OUT_TAR}"
tar -C "${stage}" -czf "${OUT_TAR}" PixelVRPlayer
+50
View File
@@ -0,0 +1,50 @@
@echo off
setlocal enabledelayedexpansion
echo ===============================================================
echo Pixel VR Player - Steam Frame Remote Updater
echo ===============================================================
echo This tool updates Pixel VR Player on your Valve Steam Frame
echo to the latest release over Wi-Fi directly from your PC.
echo (All your settings, bookmarks, and shares are kept safe).
echo ===============================================================
echo.
set "FRAME_IP=steamframe.local"
set /p "INPUT_IP=Enter Steam Frame IP address [press Enter for steamframe.local]: "
if not "!INPUT_IP!"=="" set "FRAME_IP=!INPUT_IP!"
echo.
echo Connecting to steamos@!FRAME_IP!...
echo (If prompted for a password, enter your SteamOS desktop password)
echo.
where ssh >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo [ERROR] SSH is not installed or not in PATH on this PC.
pause
exit /b 1
)
set "REMOTE_CMD=curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh -s -- --update"
ssh -o StrictHostKeyChecking=no steamos@!FRAME_IP! "!REMOTE_CMD!"
if %ERRORLEVEL% equ 0 (
echo.
echo ===============================================================
echo [SUCCESS] Pixel VR Player has been updated on your Steam Frame!
echo ===============================================================
) else (
echo.
echo ===============================================================
echo [NOTE] If connection failed, please ensure:
echo 1. Both your PC and Steam Frame are on the same Wi-Fi.
echo 2. SSH is enabled on the headset.
echo (In Desktop Mode, open Konsole and run 'passwd',
echo then 'sudo systemctl start sshd', or enable Dev Mode).
echo ===============================================================
)
echo.
pause
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Pixel VR Player - Remote updater from Mac / Linux to Steam Frame
set -euo pipefail
echo "==============================================================="
echo " Pixel VR Player - Steam Frame Remote Updater"
echo "==============================================================="
echo " This tool updates Pixel VR Player on your Valve Steam Frame"
echo " to the latest release over Wi-Fi directly from your Mac or PC."
echo " (All your settings, bookmarks, and shares are kept safe)."
echo "==============================================================="
echo ""
read -rp "Enter Steam Frame IP address [press Enter for steamframe.local]: " INPUT_IP
FRAME_IP="${INPUT_IP:-steamframe.local}"
echo ""
echo "Connecting to steamos@${FRAME_IP}..."
echo "(If prompted for a password, enter your SteamOS desktop password)"
echo ""
REMOTE_CMD="curl -fsSL https://raw.githubusercontent.com/Pixelplanet/Pixel-VR-Player/main/tools/install.sh | sh -s -- --update"
if ssh -o StrictHostKeyChecking=no "steamos@${FRAME_IP}" "${REMOTE_CMD}"; then
echo ""
echo "==============================================================="
echo " [SUCCESS] Pixel VR Player has been updated on your Steam Frame!"
echo "==============================================================="
else
echo ""
echo "==============================================================="
echo " [NOTE] If connection failed, please ensure:"
echo " 1. Both devices are on the same Wi-Fi network."
echo " 2. SSH is enabled on the headset."
echo " (In Desktop Mode, open Konsole and run 'passwd',"
echo " then 'sudo systemctl start sshd', or enable Dev Mode)."
echo "==============================================================="
fi
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# Pixel VR Player updater for Valve Steam Frame
# Downloads and installs the latest release in-place.
set -eu
REPO="Pixelplanet/Pixel-VR-Player"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
INSTALL_DIR="${PIXELVR_INSTALL_DIR:-$HOME/PixelVRPlayer}"
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/pixelvr" ]; then
INSTALL_DIR="$SCRIPT_DIR"
fi
if [ -t 1 ]; then
C_INFO=$(printf '\0033[36m')
C_OK=$(printf '\0033[32m')
C_ERR=$(printf '\0033[31m')
C_OFF=$(printf '\0033[0m')
else
C_INFO=""; C_OK=""; C_ERR=""; C_OFF=""
fi
printf '%s==>%s Checking for Pixel VR Player updates for %s...\n' "$C_INFO" "$C_OFF" "$INSTALL_DIR"
# Execute in-place update using install.sh with --update flag
if [ -f "$INSTALL_DIR/install.sh" ]; then
"$INSTALL_DIR/install.sh" --dir "$INSTALL_DIR" --update "$@"
else
curl -fsSL https://raw.githubusercontent.com/"$REPO"/main/tools/install.sh | sh -s -- --dir "$INSTALL_DIR" --update "$@"
fi
if [ $? -eq 0 ]; then
printf '\n%s ok%s Pixel VR Player update complete!\n' "$C_OK" "$C_OFF"
if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
if command -v notify-send >/dev/null 2>&1; then
notify-send "Pixel VR Player" "Updated to the latest version successfully!" || true
fi
fi
else
printf '\n%serror%s Pixel VR Player update failed.\n' "$C_ERR" "$C_OFF" >&2
exit 1
fi