mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-22 08:01:27 +02:00
- Add ThumbnailCache: a worker thread extracts 256x144 thumbnails and media metadata with FFmpeg, SBS-aware cropping, and an on-disk cache under ~/.cache/pixelvr/thumbs. - Draw thumbnails on browser cards through a new rounded-corner thumb.frag pipeline with per-card descriptor sets. - Redesign the browser as a sidebar + card grid, with horizontal stick navigation and direct shortcuts to Network/Home/Videos/Downloads/Drives. - Expand Settings: screen curvature, UI text scale and colour, accent colour and UI distance, organised into sections. - Downloads window: per-job state, progress, speed and ETA, plus pause/resume and cancel-active controls. - Decoder: multi-threaded software decode and four V4L2 output buffers. - docs: unified UI concept, redesign plan and mockups. Release packaging is now reproducible: tools/run.sh is the shipped launcher, tools/package-release.sh assembles the tarball, and a GitHub Actions workflow builds it natively on ARM64 for every v* tag. Stop committing release tarballs; they are attached to releases instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
481 lines
19 KiB
C++
481 lines
19 KiB
C++
#include "ui/settings_menu.hpp"
|
|
|
|
#include "util/logging.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
namespace pixelvr {
|
|
|
|
namespace {
|
|
|
|
// Must match XrInputState::Button ordering.
|
|
const char* kButtonNames[] = {"A", "B", "X",
|
|
"Y", "Menu", "View",
|
|
"Bumper", "Thumbstick", "Grip",
|
|
"D-Pad Up", "D-Pad Down", "D-Pad Left",
|
|
"D-Pad Right"};
|
|
constexpr int kButtonCount = 13;
|
|
const char* kDownloadDirs[] = {"~/media", "~/Downloads", "~/Videos"};
|
|
constexpr int kDownloadDirCount = 3;
|
|
|
|
const char* kLangNames[] = {"Auto", "English", "German", "French",
|
|
"Spanish", "Japanese", "Italian", "Russian",
|
|
"Chinese", "Korean"};
|
|
const char* kLangCodes[] = {"", "eng", "deu", "fra", "spa",
|
|
"jpn", "ita", "rus", "chi", "kor"};
|
|
constexpr int kLangCount = 10;
|
|
|
|
const char* kCurvatureNames[] = {"Flat (0%)", "Gentle (25%)", "Cinema (50%)",
|
|
"Deep Curve (75%)", "IMAX Wrap (100%)"};
|
|
const float kCurvatureValues[] = {0.0f, 0.25f, 0.50f, 0.75f, 1.00f};
|
|
constexpr int kCurvatureCount = 5;
|
|
|
|
const char* kUiTextScaleNames[] = {"Compact (90%)", "Standard (105%)",
|
|
"Large (120%)", "Extra Large (140%)",
|
|
"Huge (160%)"};
|
|
const float kUiTextScaleValues[] = {0.90f, 1.05f, 1.20f, 1.40f, 1.60f};
|
|
constexpr int kUiTextScaleCount = 5;
|
|
|
|
const char* kUiTextColorNames[] = {"Pure White", "Ice Cyan", "Warm Cream",
|
|
"Soft Gold", "Mint Green"};
|
|
constexpr int kUiTextColorCount = 5;
|
|
|
|
const char* kUiAccentColorNames[] = {"Cyber Cyan", "Emerald Green", "Amber Gold",
|
|
"Royal Purple", "Crimson Red"};
|
|
constexpr int kUiAccentColorCount = 5;
|
|
|
|
const char* kUiDistanceNames[] = {"Near (1.5 m)", "Comfort (1.9 m)",
|
|
"Medium (2.3 m)", "Far (2.8 m)"};
|
|
const float kUiDistanceValues[] = {1.5f, 1.9f, 2.3f, 2.8f};
|
|
constexpr int kUiDistanceCount = 4;
|
|
|
|
std::string getConfigPath() {
|
|
#ifdef _WIN32
|
|
const char* appData = std::getenv("APPDATA");
|
|
if (appData) {
|
|
return std::string(appData) + "/PixelVR/settings.conf";
|
|
}
|
|
#else
|
|
const char* home = std::getenv("HOME");
|
|
if (home) {
|
|
return std::string(home) + "/.config/pixelvr/settings.conf";
|
|
}
|
|
#endif
|
|
return "pixelvr_settings.conf";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SettingsMenu::SettingsMenu() {
|
|
loadConfig();
|
|
refresh();
|
|
}
|
|
|
|
void SettingsMenu::open() {
|
|
open_ = true;
|
|
if (section_ < 0 || section_ > 3) section_ = 0;
|
|
refresh();
|
|
}
|
|
|
|
void SettingsMenu::setSelected(int s) {
|
|
const int n = static_cast<int>(rows_.size());
|
|
selected_ = (n <= 0) ? 0 : (s < 0 ? 0 : (s >= n ? n - 1 : s));
|
|
}
|
|
|
|
void SettingsMenu::move(int delta) {
|
|
setSelected(selected_ + delta);
|
|
}
|
|
|
|
void SettingsMenu::refresh() {
|
|
rows_.clear();
|
|
|
|
if (section_ < 0) { // top-level section list
|
|
title_ = "Settings";
|
|
rows_.push_back({"Display & Projection", ">", Action::EnterDisplay});
|
|
rows_.push_back({"Controls & Bindings", ">", Action::EnterControls});
|
|
rows_.push_back({"Audio / Video Tracks", ">", Action::EnterStreams});
|
|
rows_.push_back({"System & Debug", ">", Action::EnterSystem});
|
|
rows_.push_back({"Recenter Screen View", "[ Trigger to Recenter ]",
|
|
Action::RecenterScreen});
|
|
rows_.push_back({"Close Settings", "[ Back to Player ]", Action::Close});
|
|
return;
|
|
}
|
|
|
|
if (section_ == 0) {
|
|
title_ = "Settings > Display & Cinema";
|
|
const char* projNames[] = {"Flat Screen", "360 Equirectangular",
|
|
"180 Equirectangular"};
|
|
const char* stereoNames[] = {"2D Mono", "Side-by-Side (SBS)",
|
|
"Top-Bottom (OU)"};
|
|
std::ostringstream distStream;
|
|
distStream << std::fixed;
|
|
distStream.precision(1);
|
|
distStream << screenDistance_ << " m";
|
|
rows_.push_back({"Projection Mode", projNames[projectionMode_ % 3],
|
|
Action::CycleProjection});
|
|
rows_.push_back({"Stereo 3D Format", stereoNames[stereoMode_ % 3],
|
|
Action::CycleStereo});
|
|
rows_.push_back({"Cinema Screen Curve", kCurvatureNames[curvatureIdx_ % kCurvatureCount],
|
|
Action::CycleCurvature});
|
|
rows_.push_back({"Screen Distance", distStream.str(),
|
|
Action::CycleDistance});
|
|
rows_.push_back({"Swap Left/Right Eye", swapEyes_ ? "On" : "Off",
|
|
Action::ToggleSwapEyes});
|
|
rows_.push_back({"Recenter Screen View", "Recenter Now",
|
|
Action::RecenterScreen});
|
|
} else if (section_ == 1) {
|
|
title_ = "Settings > UI & Appearance";
|
|
rows_.push_back({"UI Text Size",
|
|
kUiTextScaleNames[uiTextScaleIdx_ % kUiTextScaleCount],
|
|
Action::CycleUiTextScale});
|
|
rows_.push_back({"UI Text Color",
|
|
kUiTextColorNames[uiTextColorIdx_ % kUiTextColorCount],
|
|
Action::CycleUiTextColor});
|
|
rows_.push_back({"UI Accent Theme",
|
|
kUiAccentColorNames[uiAccentColorIdx_ % kUiAccentColorCount],
|
|
Action::CycleUiAccentColor});
|
|
rows_.push_back({"UI Panel Distance",
|
|
kUiDistanceNames[uiDistanceIdx_ % kUiDistanceCount],
|
|
Action::CycleUiDistance});
|
|
rows_.push_back({"Download Folder",
|
|
kDownloadDirs[downloadDirIdx_ % kDownloadDirCount],
|
|
Action::CycleDownloadDir});
|
|
rows_.push_back({"Debug Statistics", showStats_ ? "On" : "Off",
|
|
Action::ToggleStats});
|
|
} else if (section_ == 2) {
|
|
title_ = "Settings > Audio & Video Tracks";
|
|
Row v{"Video Track",
|
|
videoStreams_.empty()
|
|
? "(none)"
|
|
: videoStreams_[std::min<std::size_t>(
|
|
videoSel_, videoStreams_.size() - 1)].second,
|
|
Action::EnterVideoList};
|
|
v.enabled = videoStreams_.size() > 1; // nothing to pick with 0/1 track
|
|
if (!v.enabled) v.action = Action::None;
|
|
rows_.push_back(v);
|
|
Row a{"Audio Track",
|
|
audioStreams_.empty()
|
|
? "(none)"
|
|
: audioStreams_[std::min<std::size_t>(
|
|
audioSel_, audioStreams_.size() - 1)].second,
|
|
Action::EnterAudioList};
|
|
a.enabled = audioStreams_.size() > 1;
|
|
if (!a.enabled) a.action = Action::None;
|
|
rows_.push_back(a);
|
|
rows_.push_back({"Preferred Audio Language",
|
|
kLangNames[audioLangIdx_ % kLangCount],
|
|
Action::EnterLangList});
|
|
} else if (section_ == 3) {
|
|
title_ = "Settings > Controls & Bindings";
|
|
auto btnLabel = [&](int func, int btn) -> std::string {
|
|
return listeningFunc_ == func
|
|
? std::string("[ press a button ]")
|
|
: std::string(kButtonNames[btn % kButtonCount]);
|
|
};
|
|
rows_.push_back({"Play / Pause", btnLabel(0, bindPlayBtn_),
|
|
Action::ListenPlay});
|
|
rows_.push_back({"Open Menu / Browser", btnLabel(1, bindMenuBtn_),
|
|
Action::ListenMenu});
|
|
rows_.push_back({"Recenter View", btnLabel(2, bindRecenterBtn_),
|
|
Action::ListenRecenter});
|
|
rows_.push_back({"Select / Click", "Trigger", Action::None});
|
|
rows_.push_back({"Controller Models", drawModels_ ? "On" : "Off",
|
|
Action::ToggleModels});
|
|
} else if (section_ == 10) {
|
|
title_ = "Settings > Select Video Track";
|
|
for (std::size_t i = 0; i < videoStreams_.size(); ++i) {
|
|
Row r{videoStreams_[i].second,
|
|
static_cast<int>(i) == videoSel_ ? "ACTIVE" : "Select",
|
|
Action::PickVideoStream};
|
|
r.data = static_cast<int>(i);
|
|
rows_.push_back(r);
|
|
}
|
|
rows_.push_back({"< Back to Tracks", "", Action::Back});
|
|
} else if (section_ == 11) {
|
|
title_ = "Settings > Select Audio Track";
|
|
for (std::size_t i = 0; i < audioStreams_.size(); ++i) {
|
|
Row r{audioStreams_[i].second,
|
|
static_cast<int>(i) == audioSel_ ? "ACTIVE" : "Select",
|
|
Action::PickAudioStream};
|
|
r.data = static_cast<int>(i);
|
|
rows_.push_back(r);
|
|
}
|
|
rows_.push_back({"< Back to Tracks", "", Action::Back});
|
|
} else { // section_ == 12: preferred-language list
|
|
title_ = "Settings > Preferred Audio Language";
|
|
for (int i = 0; i < kLangCount; ++i) {
|
|
Row r{kLangNames[i], i == audioLangIdx_ ? "ACTIVE" : "Select",
|
|
Action::PickLanguage};
|
|
r.data = i;
|
|
rows_.push_back(r);
|
|
}
|
|
rows_.push_back({"< Back to Tracks", "", Action::Back});
|
|
}
|
|
}
|
|
|
|
SettingsMenu::Action SettingsMenu::activate() {
|
|
if (selected_ < 0 || selected_ >= static_cast<int>(rows_.size())) {
|
|
return Action::None;
|
|
}
|
|
const Action act = rows_[selected_].action;
|
|
switch (act) {
|
|
case Action::EnterDisplay: section_ = 0; selected_ = 0; refresh(); break;
|
|
case Action::EnterControls: section_ = 1; selected_ = 0; refresh(); break;
|
|
case Action::EnterStreams: section_ = 2; selected_ = 0; refresh(); break;
|
|
case Action::EnterSystem: section_ = 3; selected_ = 0; refresh(); break;
|
|
case Action::EnterVideoList: section_ = 10; selected_ = 0; refresh(); break;
|
|
case Action::EnterAudioList: section_ = 11; selected_ = 0; refresh(); break;
|
|
case Action::EnterLangList: section_ = 12; selected_ = 0; refresh(); break;
|
|
case Action::Back:
|
|
section_ = (section_ >= 10) ? 2 : 0; // list -> Tracks; else -> Display
|
|
selected_ = 0;
|
|
refresh();
|
|
break;
|
|
case Action::ListenPlay:
|
|
listeningFunc_ = 0;
|
|
status_ = "Press any controller button for Play / Pause";
|
|
refresh();
|
|
break;
|
|
case Action::ListenMenu:
|
|
listeningFunc_ = 1;
|
|
status_ = "Press any controller button for Open Menu";
|
|
refresh();
|
|
break;
|
|
case Action::ListenRecenter:
|
|
listeningFunc_ = 2;
|
|
status_ = "Press any controller button for Recenter";
|
|
refresh();
|
|
break;
|
|
case Action::CycleDownloadDir:
|
|
downloadDirIdx_ = (downloadDirIdx_ + 1) % kDownloadDirCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleProjection:
|
|
projectionMode_ = (projectionMode_ + 1) % 3;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleStereo:
|
|
stereoMode_ = (stereoMode_ + 1) % 3;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::ToggleSwapEyes:
|
|
swapEyes_ = !swapEyes_;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::ToggleModels:
|
|
drawModels_ = !drawModels_;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::ToggleStats:
|
|
showStats_ = !showStats_;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleDistance:
|
|
screenDistance_ += 0.5f;
|
|
if (screenDistance_ > 5.0f) screenDistance_ = 1.5f;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleCurvature:
|
|
curvatureIdx_ = (curvatureIdx_ + 1) % kCurvatureCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleUiTextScale:
|
|
uiTextScaleIdx_ = (uiTextScaleIdx_ + 1) % kUiTextScaleCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleUiTextColor:
|
|
uiTextColorIdx_ = (uiTextColorIdx_ + 1) % kUiTextColorCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleUiAccentColor:
|
|
uiAccentColorIdx_ = (uiAccentColorIdx_ + 1) % kUiAccentColorCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::CycleUiDistance:
|
|
uiDistanceIdx_ = (uiDistanceIdx_ + 1) % kUiDistanceCount;
|
|
saveConfig();
|
|
refresh();
|
|
break;
|
|
case Action::PickVideoStream:
|
|
videoSel_ = rows_[selected_].data;
|
|
section_ = 2;
|
|
selected_ = 0;
|
|
refresh();
|
|
break;
|
|
case Action::PickAudioStream:
|
|
audioSel_ = rows_[selected_].data;
|
|
section_ = 2;
|
|
selected_ = 0;
|
|
refresh();
|
|
break;
|
|
case Action::PickLanguage:
|
|
audioLangIdx_ = rows_[selected_].data;
|
|
saveConfig();
|
|
section_ = 2;
|
|
selected_ = 0;
|
|
refresh();
|
|
break;
|
|
case Action::RecenterScreen:
|
|
break;
|
|
case Action::Close:
|
|
close();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return act;
|
|
}
|
|
|
|
void SettingsMenu::setStreams(
|
|
const std::vector<std::pair<int, std::string>>& video,
|
|
const std::vector<std::pair<int, std::string>>& audio, int currentVideo,
|
|
int currentAudio) {
|
|
videoStreams_ = video;
|
|
audioStreams_ = audio;
|
|
videoSel_ = 0;
|
|
for (std::size_t i = 0; i < video.size(); ++i)
|
|
if (video[i].first == currentVideo) videoSel_ = static_cast<int>(i);
|
|
audioSel_ = 0;
|
|
for (std::size_t i = 0; i < audio.size(); ++i)
|
|
if (audio[i].first == currentAudio) audioSel_ = static_cast<int>(i);
|
|
if (open_) refresh();
|
|
}
|
|
|
|
int SettingsMenu::selectedVideoStream() const {
|
|
return (videoSel_ >= 0 && videoSel_ < static_cast<int>(videoStreams_.size()))
|
|
? videoStreams_[videoSel_].first
|
|
: -1;
|
|
}
|
|
|
|
int SettingsMenu::selectedAudioStream() const {
|
|
return (audioSel_ >= 0 && audioSel_ < static_cast<int>(audioStreams_.size()))
|
|
? audioStreams_[audioSel_].first
|
|
: -1;
|
|
}
|
|
|
|
std::string SettingsMenu::audioLanguage() const {
|
|
return kLangCodes[audioLangIdx_ % kLangCount];
|
|
}
|
|
|
|
void SettingsMenu::assignListening(int button) {
|
|
if (listeningFunc_ < 0) return;
|
|
const int b = (button < 0 || button >= kButtonCount) ? 0 : button;
|
|
if (listeningFunc_ == 0) bindPlayBtn_ = b;
|
|
else if (listeningFunc_ == 1) bindMenuBtn_ = b;
|
|
else if (listeningFunc_ == 2) bindRecenterBtn_ = b;
|
|
listeningFunc_ = -1;
|
|
status_ = "Bound to " + std::string(kButtonNames[b]);
|
|
saveConfig();
|
|
refresh();
|
|
}
|
|
|
|
std::string SettingsMenu::downloadDir() const {
|
|
const char* home = std::getenv("HOME");
|
|
const std::string base = home ? home : ".";
|
|
const char* d = kDownloadDirs[downloadDirIdx_ % kDownloadDirCount];
|
|
return d[0] == '~' ? base + (d + 1) : std::string(d);
|
|
}
|
|
|
|
float SettingsMenu::screenCurvature() const {
|
|
return kCurvatureValues[curvatureIdx_ % kCurvatureCount];
|
|
}
|
|
|
|
void SettingsMenu::setCurvatureIdx(int idx) {
|
|
curvatureIdx_ = ((idx % kCurvatureCount) + kCurvatureCount) % kCurvatureCount;
|
|
saveConfig();
|
|
refresh();
|
|
}
|
|
|
|
float SettingsMenu::uiTextScale() const {
|
|
return kUiTextScaleValues[uiTextScaleIdx_ % kUiTextScaleCount];
|
|
}
|
|
|
|
float SettingsMenu::uiDistance() const {
|
|
return kUiDistanceValues[uiDistanceIdx_ % kUiDistanceCount];
|
|
}
|
|
|
|
void SettingsMenu::loadConfig() {
|
|
const std::string path = getConfigPath();
|
|
std::ifstream file(path);
|
|
if (!file.is_open()) {
|
|
return;
|
|
}
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
auto eq = line.find('=');
|
|
if (eq == std::string::npos) continue;
|
|
std::string key = line.substr(0, eq);
|
|
std::string val = line.substr(eq + 1);
|
|
if (key == "projection") projectionMode_ = std::clamp(std::stoi(val), 0, 2);
|
|
else if (key == "stereo") stereoMode_ = std::clamp(std::stoi(val), 0, 2);
|
|
else if (key == "swap_eyes") swapEyes_ = (val == "1");
|
|
else if (key == "draw_models") drawModels_ = (val == "1");
|
|
else if (key == "show_stats") showStats_ = (val == "1");
|
|
else if (key == "distance") screenDistance_ = std::clamp(std::stof(val), 1.0f, 10.0f);
|
|
else if (key == "bind_play_btn") bindPlayBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1);
|
|
else if (key == "bind_menu_btn") bindMenuBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1);
|
|
else if (key == "bind_recenter_btn") {
|
|
const int b = std::clamp(std::stoi(val), 0, kButtonCount - 1);
|
|
// Grip used to default to recenter; it now toggles the laser pointer,
|
|
// so a saved grip binding is migrated to the new default (Bumper).
|
|
bindRecenterBtn_ = (b == 8) ? 6 : b;
|
|
}
|
|
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);
|
|
else if (key == "ui_text_scale") uiTextScaleIdx_ = std::clamp(std::stoi(val), 0, kUiTextScaleCount - 1);
|
|
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);
|
|
}
|
|
PIXELVR_LOG_INFO("Loaded settings from %s", path.c_str());
|
|
}
|
|
|
|
void SettingsMenu::saveConfig() const {
|
|
const std::string path = getConfigPath();
|
|
try {
|
|
std::filesystem::path p(path);
|
|
if (p.has_parent_path()) {
|
|
std::filesystem::create_directories(p.parent_path());
|
|
}
|
|
std::ofstream file(path);
|
|
if (file.is_open()) {
|
|
file << "projection=" << projectionMode_ << "\n";
|
|
file << "stereo=" << stereoMode_ << "\n";
|
|
file << "swap_eyes=" << (swapEyes_ ? 1 : 0) << "\n";
|
|
file << "draw_models=" << (drawModels_ ? 1 : 0) << "\n";
|
|
file << "show_stats=" << (showStats_ ? 1 : 0) << "\n";
|
|
file << "distance=" << screenDistance_ << "\n";
|
|
file << "bind_play_btn=" << bindPlayBtn_ << "\n";
|
|
file << "bind_menu_btn=" << bindMenuBtn_ << "\n";
|
|
file << "bind_recenter_btn=" << bindRecenterBtn_ << "\n";
|
|
file << "download_dir=" << downloadDirIdx_ << "\n";
|
|
file << "audio_lang=" << audioLangIdx_ << "\n";
|
|
file << "curvature=" << curvatureIdx_ << "\n";
|
|
file << "ui_text_scale=" << uiTextScaleIdx_ << "\n";
|
|
file << "ui_text_color=" << uiTextColorIdx_ << "\n";
|
|
file << "ui_accent_color=" << uiAccentColorIdx_ << "\n";
|
|
file << "ui_distance=" << uiDistanceIdx_ << "\n";
|
|
}
|
|
} catch (const std::exception& e) {
|
|
PIXELVR_LOG_WARN("Could not save settings: %s", e.what());
|
|
}
|
|
}
|
|
|
|
} // namespace pixelvr
|