Add UUID to apps w/ add option to use App's id for virtual display (resolves #48)

This commit is contained in:
Yukino Song
2024-09-22 05:52:00 +08:00
parent 4d5cc526b5
commit 80fb7efc3d
8 changed files with 213 additions and 158 deletions

View File

@@ -395,38 +395,8 @@ namespace confighttp {
pt::read_json(ss, inputTree); pt::read_json(ss, inputTree);
pt::read_json(config::stream.file_apps, fileTree); pt::read_json(config::stream.file_apps, fileTree);
if (inputTree.get_child("prep-cmd").empty()) { proc::migrate_apps(&fileTree, &inputTree);
inputTree.erase("prep-cmd");
}
if (inputTree.get_child("detached").empty()) {
inputTree.erase("detached");
}
auto &apps_node = fileTree.get_child("apps"s);
int index = inputTree.get<int>("index");
inputTree.erase("index");
if (index == -1) {
apps_node.push_back(std::make_pair("", inputTree));
}
else {
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
pt::ptree newApps;
int i = 0;
for (const auto &kv : apps_node) {
if (i == index) {
newApps.push_back(std::make_pair("", inputTree));
}
else {
newApps.push_back(std::make_pair("", kv.second));
}
i++;
}
fileTree.erase("apps");
fileTree.push_back(std::make_pair("apps", newApps));
}
pt::write_json(config::stream.file_apps, fileTree); pt::write_json(config::stream.file_apps, fileTree);
} }
catch (std::exception &e) { catch (std::exception &e) {
@@ -450,33 +420,38 @@ namespace confighttp {
pt::ptree outputTree; pt::ptree outputTree;
auto g = util::fail_guard([&]() { auto g = util::fail_guard([&]() {
std::ostringstream data; std::ostringstream data;
pt::write_json(data, outputTree); pt::write_json(data, outputTree);
response->write(data.str()); response->write(data.str());
}); });
auto args = request->parse_query_string();
if (
args.find("uuid"s) == std::end(args)
) {
outputTree.put("status", false);
outputTree.put("error", "Missing a required launch parameter");
return;
}
auto uuid = nvhttp::get_arg(args, "uuid");
pt::ptree fileTree; pt::ptree fileTree;
try { try {
pt::read_json(config::stream.file_apps, fileTree); pt::read_json(config::stream.file_apps, fileTree);
auto &apps_node = fileTree.get_child("apps"s); auto &apps_node = fileTree.get_child("apps"s);
int index = stoi(request->path_match[1]);
if (index < 0) { // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
outputTree.put("status", "false"); pt::ptree newApps;
outputTree.put("error", "Invalid Index"); for (const auto &kv : apps_node) {
return; auto app_uuid = kv.second.get_optional<std::string>("uuid"s);
} if (!app_uuid || app_uuid.value() != uuid) {
else { newApps.push_back(std::make_pair("", kv.second));
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
pt::ptree newApps;
int i = 0;
for (const auto &kv : apps_node) {
if (i++ != index) {
newApps.push_back(std::make_pair("", kv.second));
}
} }
fileTree.erase("apps");
fileTree.push_back(std::make_pair("apps", newApps));
} }
fileTree.erase("apps");
fileTree.push_back(std::make_pair("apps", newApps));
pt::write_json(config::stream.file_apps, fileTree); pt::write_json(config::stream.file_apps, fileTree);
} }
catch (std::exception &e) { catch (std::exception &e) {
@@ -951,7 +926,7 @@ namespace confighttp {
auto args = request->parse_query_string(); auto args = request->parse_query_string();
if ( if (
args.find("id"s) == std::end(args) args.find("uuid"s) == std::end(args)
) { ) {
outputTree.put("status", false); outputTree.put("status", false);
outputTree.put("error", "Missing a required launch parameter"); outputTree.put("error", "Missing a required launch parameter");
@@ -959,41 +934,42 @@ namespace confighttp {
return; return;
} }
auto idx_str = nvhttp::get_arg(args, "id"); auto uuid = nvhttp::get_arg(args, "uuid");
auto idx = util::from_view(idx_str);
const auto& apps = proc::proc.get_apps(); const auto& apps = proc::proc.get_apps();
if (idx >= apps.size()) { for (auto& app : apps) {
BOOST_LOG(error) << "Couldn't find app with index ["sv << idx_str << ']'; if (app.uuid == uuid) {
outputTree.put("status", false); auto appid = util::from_view(app.id);
outputTree.put("error", "Cannot find requested application");
return; crypto::named_cert_t named_cert {
.name = "",
.uuid = http::unique_id,
.perm = crypto::PERM::_all,
};
BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv;
auto launch_session = nvhttp::make_launch_session(true, appid, args, &named_cert);
auto err = proc::proc.execute(appid, app, launch_session);
if (err) {
outputTree.put("status", false);
outputTree.put("error",
err == 503
? "Failed to initialize video capture/encoding. Is a display connected and turned on?"
: "Failed to start the specified application");
return;
} else {
outputTree.put("status", true);
}
return;
}
} }
auto& app = apps[idx]; BOOST_LOG(error) << "Couldn't find app with uuid ["sv << uuid << ']';
auto appid = util::from_view(app.id); outputTree.put("status", false);
outputTree.put("error", "Cannot find requested application");
crypto::named_cert_t named_cert {
.name = "",
.uuid = http::unique_id,
.perm = crypto::PERM::_all,
};
BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv;
auto launch_session = nvhttp::make_launch_session(true, appid, args, &named_cert);
auto err = proc::proc.execute(appid, app, launch_session);
if (err) {
outputTree.put("status", false);
outputTree.put("error",
err == 503
? "Failed to initialize video capture/encoding. Is a display connected and turned on?"
: "Failed to start the specified application");
return;
} else {
outputTree.put("status", true);
}
} }
void void
@@ -1086,22 +1062,22 @@ namespace confighttp {
server.resource["^/api/pin$"]["POST"] = savePin; server.resource["^/api/pin$"]["POST"] = savePin;
server.resource["^/api/otp$"]["GET"] = getOTP; server.resource["^/api/otp$"]["GET"] = getOTP;
server.resource["^/api/apps$"]["GET"] = getApps; server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/apps$"]["POST"] = saveApp;
server.resource["^/api/apps/delete$"]["POST"] = deleteApp;
server.resource["^/api/apps/launch$"]["POST"] = launchApp;
server.resource["^/api/apps/close$"]["POST"] = closeApp;
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["GET"] = getConfig;
server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/config$"]["POST"] = saveConfig;
server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/configLocale$"]["GET"] = getLocale;
server.resource["^/api/restart$"]["POST"] = restart; server.resource["^/api/restart$"]["POST"] = restart;
server.resource["^/api/quit$"]["POST"] = quit; server.resource["^/api/quit$"]["POST"] = quit;
server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/password$"]["POST"] = savePassword;
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
server.resource["^/api/clients/list$"]["GET"] = listClients; server.resource["^/api/clients/list$"]["GET"] = listClients;
server.resource["^/api/clients/update$"]["POST"] = updateClient; server.resource["^/api/clients/update$"]["POST"] = updateClient;
server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/clients/disconnect$"]["POST"] = disconnect; server.resource["^/api/clients/disconnect$"]["POST"] = disconnect;
server.resource["^/api/apps/launch$"]["POST"] = launchApp;
server.resource["^/api/apps/close$"]["POST"] = closeApp;
server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage; server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-apollo-45.png$"]["GET"] = getSunshineLogoImage; server.resource["^/images/logo-apollo-45.png$"]["GET"] = getSunshineLogoImage;

View File

@@ -211,13 +211,16 @@ namespace proc {
VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name));
} }
auto device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id); auto device_uuid_str = _app.use_app_identity ? _app.uuid : launch_session->unique_id;
auto device_name = _app.use_app_identity ? _app.name : launch_session->device_name;
auto device_uuid = uuid_util::uuid_t::parse(device_uuid_str);
memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID)); memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID));
std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( std::wstring vdisplayName = VDISPLAY::createVirtualDisplay(
launch_session->unique_id.c_str(), device_uuid_str.c_str(),
launch_session->device_name.c_str(), device_name.c_str(),
render_width, render_width,
render_height, render_height,
launch_session->fps ? launch_session->fps : 60, launch_session->fps ? launch_session->fps : 60,
@@ -794,6 +797,63 @@ namespace proc {
return std::make_tuple(id_no_index, id_with_index); return std::make_tuple(id_no_index, id_with_index);
} }
void
migrate_apps(pt::ptree* fileTree_p, pt::ptree* inputTree_p) {
std::string new_app_uuid;
if (inputTree_p) {
auto inputTree = *inputTree_p;
auto input_uuid = inputTree_p->get_optional<std::string>("uuid"s);
if (input_uuid && !input_uuid.value().empty()) {
new_app_uuid = input_uuid.value();
} else {
new_app_uuid = uuid_util::uuid_t::generate().string();
inputTree_p->erase("uuid");
inputTree_p->put("uuid", new_app_uuid);
}
if (inputTree_p->get_child("prep-cmd").empty()) {
inputTree_p->erase("prep-cmd");
}
if (inputTree_p->get_child("detached").empty()) {
inputTree_p->erase("detached");
}
inputTree_p->erase("launching");
inputTree_p->erase("index");
}
auto &apps_node = fileTree_p->get_child("apps"s);
pt::ptree newApps;
for (auto &kv : apps_node) {
// Check if we have apps that have not got an uuid assigned
auto app_uuid = kv.second.get_optional<std::string>("uuid"s);
if (!app_uuid || app_uuid.value().empty()) {
kv.second.erase("uuid");
kv.second.put("uuid", uuid_util::uuid_t::generate().string());
kv.second.erase("launching");
newApps.push_back(std::make_pair("", kv.second));
} else {
if (!new_app_uuid.empty() && app_uuid.value() == new_app_uuid) {
newApps.push_back(std::make_pair("", *inputTree_p));
new_app_uuid.clear();
} else {
newApps.push_back(std::make_pair("", kv.second));
}
}
}
// Finally add the new app
if (!new_app_uuid.empty()) {
newApps.push_back(std::make_pair("", *inputTree_p));
}
fileTree_p->erase("apps");
fileTree_p->push_back(std::make_pair("apps", newApps));
}
std::optional<proc::proc_t> std::optional<proc::proc_t>
parse(const std::string &file_name) { parse(const std::string &file_name) {
pt::ptree tree; pt::ptree tree;
@@ -816,6 +876,20 @@ namespace proc {
for (auto &[_, app_node] : apps_node) { for (auto &[_, app_node] : apps_node) {
proc::ctx_t ctx; proc::ctx_t ctx;
auto app_uuid = app_node.get_optional<std::string>("uuid"s);
if (!app_uuid) {
// We need an upgrade to the app list
try {
migrate_apps(&tree, nullptr);
pt::write_json(file_name, tree);
return parse(file_name);
} catch (std::exception &e) {
BOOST_LOG(warning) << "Error happened wilie migrating the app list: "sv << e.what();
return std::nullopt;
}
}
auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s); auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s);
auto detached_nodes_opt = app_node.get_child_optional("detached"s); auto detached_nodes_opt = app_node.get_child_optional("detached"s);
auto exclude_global_prep = app_node.get_optional<bool>("exclude-global-prep-cmd"s); auto exclude_global_prep = app_node.get_optional<bool>("exclude-global-prep-cmd"s);
@@ -831,6 +905,9 @@ namespace proc {
auto virtual_display = app_node.get_optional<bool>("virtual-display"s); auto virtual_display = app_node.get_optional<bool>("virtual-display"s);
auto virtual_display_primary = app_node.get_optional<bool>("virtual-display-primary"s); auto virtual_display_primary = app_node.get_optional<bool>("virtual-display-primary"s);
auto resolution_scale_factor = app_node.get_optional<int>("scale-factor"s); auto resolution_scale_factor = app_node.get_optional<int>("scale-factor"s);
auto use_app_identity = app_node.get_optional<bool>("use-app-identity"s);
ctx.uuid = app_uuid.value();
std::vector<proc::cmd_t> prep_cmds; std::vector<proc::cmd_t> prep_cmds;
if (!exclude_global_prep.value_or(false)) { if (!exclude_global_prep.value_or(false)) {
@@ -901,6 +978,7 @@ namespace proc {
ctx.virtual_display = virtual_display.value_or(false); ctx.virtual_display = virtual_display.value_or(false);
ctx.virtual_display_primary = virtual_display_primary.value_or(true); ctx.virtual_display_primary = virtual_display_primary.value_or(true);
ctx.scale_factor = resolution_scale_factor.value_or(100); ctx.scale_factor = resolution_scale_factor.value_or(100);
ctx.use_app_identity = use_app_identity.value_or(false);
auto possible_ids = calculate_app_id(name, ctx.image_path, i++); auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
if (ids.count(std::get<0>(possible_ids)) == 0) { if (ids.count(std::get<0>(possible_ids)) == 0) {
@@ -923,11 +1001,13 @@ namespace proc {
#ifdef _WIN32 #ifdef _WIN32
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
proc::ctx_t ctx; proc::ctx_t ctx;
// ctx.uuid = ""; // We're not using uuid for this special entry
ctx.name = "Virtual Display"; ctx.name = "Virtual Display";
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png"); ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
ctx.virtual_display = true; ctx.virtual_display = true;
ctx.virtual_display_primary = true; ctx.virtual_display_primary = true;
ctx.scale_factor = 100; ctx.scale_factor = 100;
ctx.use_app_identity = false;
ctx.elevated = false; ctx.elevated = false;
ctx.auto_detach = true; ctx.auto_detach = true;

View File

@@ -12,6 +12,7 @@
#include <unordered_map> #include <unordered_map>
#include <boost/process.hpp> #include <boost/process.hpp>
#include <boost/property_tree/ptree.hpp>
#include "config.h" #include "config.h"
#include "platform/common.h" #include "platform/common.h"
@@ -59,6 +60,7 @@ namespace proc {
*/ */
std::vector<std::string> detached; std::vector<std::string> detached;
std::string uuid;
std::string name; std::string name;
std::string cmd; std::string cmd;
std::string working_dir; std::string working_dir;
@@ -70,6 +72,7 @@ namespace proc {
bool wait_all; bool wait_all;
bool virtual_display; bool virtual_display;
bool virtual_display_primary; bool virtual_display_primary;
bool use_app_identity;
int scale_factor; int scale_factor;
std::chrono::seconds exit_timeout; std::chrono::seconds exit_timeout;
}; };
@@ -150,6 +153,8 @@ namespace proc {
validate_app_image_path(std::string app_image_path); validate_app_image_path(std::string app_image_path);
void void
refresh(const std::string &file_name); refresh(const std::string &file_name);
void
migrate_apps(boost::property_tree::ptree* fileTree_p, boost::property_tree::ptree* inputTree_p);
std::optional<proc::proc_t> std::optional<proc::proc_t>
parse(const std::string &file_name); parse(const std::string &file_name);

View File

@@ -86,16 +86,16 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(app,i) in apps" :key="i"> <tr v-for="(app,i) in apps" :key="app.uuid">
<td>{{app.name}}</td> <td>{{app.name}}</td>
<td> <td>
<button class="btn btn-success me-2" :disabled="app.launching" @click="launchApp(i)"> <button class="btn btn-success me-2" :disabled="app.launching" @click="launchApp(app)">
<i class="fas fa-play"></i> {{ $t('apps.launch') }} <i class="fas fa-play"></i> {{ $t('apps.launch') }}
</button> </button>
<button class="btn btn-primary me-2" :disabled="app.launching" @click="editApp(i)"> <button class="btn btn-primary me-2" :disabled="app.launching" @click="editApp(app)">
<i class="fas fa-edit"></i> {{ $t('apps.edit') }} <i class="fas fa-edit"></i> {{ $t('apps.edit') }}
</button> </button>
<button class="btn btn-danger" :disabled="app.launching" @click="showDeleteForm(i)"> <button class="btn btn-danger" :disabled="app.launching" @click="showDeleteForm(app)">
<i class="fas fa-trash"></i> {{ $t('apps.delete') }} <i class="fas fa-trash"></i> {{ $t('apps.delete') }}
</button> </button>
</td> </td>
@@ -274,16 +274,23 @@
<div class="mb-3 form-check" v-if="platform === 'windows'"> <div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="virtualDisplay" class="form-check-label">{{ $t('apps.virtual_display') }}</label> <label for="virtualDisplay" class="form-check-label">{{ $t('apps.virtual_display') }}</label>
<input type="checkbox" class="form-check-input" id="virtualDisplay" v-model="editForm['virtual-display']" <input type="checkbox" class="form-check-input" id="virtualDisplay" v-model="editForm['virtual-display']"
true-value="true" false-value="false" /> true-value="true" false-value="false" />
<div class="form-text">{{ $t('apps.virtual_display_desc') }}</div> <div class="form-text">{{ $t('apps.virtual_display_desc') }}</div>
</div> </div>
<!-- set virtual display to primary --> <!-- set virtual display to primary -->
<div class="mb-3 form-check" v-if="platform === 'windows' && editForm['virtual-display'] == 'true'"> <div class="mb-3 form-check" v-if="platform === 'windows' && editForm['virtual-display'] == 'true'">
<label for="virtualDisplayPrimary" class="form-check-label">{{ $t('apps.virtual_display_primary') }}</label> <label for="virtualDisplayPrimary" class="form-check-label">{{ $t('apps.virtual_display_primary') }}</label>
<input type="checkbox" class="form-check-input" id="virtualDisplayPrimary" v-model="editForm['virtual-display-primary']" <input type="checkbox" class="form-check-input" id="virtualDisplayPrimary" v-model="editForm['virtual-display-primary']"
true-value="true" false-value="false" /> true-value="true" false-value="false" />
<div class="form-text">{{ $t('apps.virtual_display_primary_desc') }}</div> <div class="form-text">{{ $t('apps.virtual_display_primary_desc') }}</div>
</div> </div>
<!-- use app identity -->
<div class="mb-3 form-check">
<label for="useAppIdentity" class="form-check-label">{{ $t('apps.use_app_identity') }}</label>
<input type="checkbox" class="form-check-input" id="useAppIdentity" v-model="editForm['use-app-identity']"
true-value="true" false-value="false" />
<div class="form-text">{{ $t('apps.use_app_identity_desc') }}</div>
</div>
<!-- resolution scale factor --> <!-- resolution scale factor -->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="resolutionScaleFactor" class="form-label">{{ $t('apps.resolution_scale_factor') }}: {{editForm['scale-factor']}}%</label> <label for="resolutionScaleFactor" class="form-label">{{ $t('apps.resolution_scale_factor') }}: {{editForm['scale-factor']}}%</label>
@@ -392,6 +399,23 @@
import PlatformLayout from './PlatformLayout.vue' import PlatformLayout from './PlatformLayout.vue'
import { Dropdown } from 'bootstrap/dist/js/bootstrap' import { Dropdown } from 'bootstrap/dist/js/bootstrap'
const newApp = {
name: "",
output: "",
cmd: [],
"exclude-global-prep-cmd": false,
elevated: false,
"auto-detach": true,
"wait-all": true,
"exit-timeout": 5,
"prep-cmd": [],
detached: [],
"image-path": "",
"virtual-display-primary": true,
"scale-factor": "100",
"use-app-identity": false
}
const app = createApp({ const app = createApp({
components: { components: {
Navbar, Navbar,
@@ -429,30 +453,13 @@
}); });
}, },
newApp() { newApp() {
this.editForm = { this.editForm = Object.assign({}, newApp);
name: "",
output: "",
cmd: [],
index: -1,
"exclude-global-prep-cmd": false,
elevated: false,
"auto-detach": true,
"wait-all": true,
"exit-timeout": 5,
"prep-cmd": [],
detached: [],
"image-path": "",
"virtual-display-primary": true,
"scale-factor": "100"
};
this.editForm.index = -1;
this.showEditForm = true; this.showEditForm = true;
}, },
launchApp(id) { launchApp(app) {
const app = this.apps[id];
if (confirm(this.$t('apps.launch_warning'))) { if (confirm(this.$t('apps.launch_warning'))) {
app.launching = true; app.launching = true;
fetch("/api/apps/launch?id=" + id, { fetch("/api/apps/launch?uuid=" + app.uuid, {
credentials: 'include', credentials: 'include',
method: "POST", method: "POST",
}) })
@@ -467,56 +474,31 @@
.finally(() => app.launching = false); .finally(() => app.launching = false);
} }
}, },
editApp(id) { editApp(app) {
this.editForm = JSON.parse(JSON.stringify(this.apps[id])); this.editForm = Object.assign({}, newApp, JSON.parse(JSON.stringify(app)));
this.editForm.index = id;
if (this.editForm["prep-cmd"] === undefined)
this.editForm["prep-cmd"] = [];
if (this.editForm["detached"] === undefined)
this.editForm["detached"] = [];
if (this.editForm["exclude-global-prep-cmd"] === undefined)
this.editForm["exclude-global-prep-cmd"] = [];
if (this.editForm["elevated"] === undefined && this.platform === 'windows') {
this.editForm["elevated"] = [];
}
if (this.editForm["auto-detach"] === undefined) {
this.editForm["auto-detach"] = true;
}
if (this.editForm["wait-all"] === undefined) {
this.editForm["wait-all"] = true;
}
if (this.editForm["exit-timeout"] === undefined) {
this.editForm["exit-timeout"] = 5;
}
if (typeof this.editForm["virtual-display-primary"] === "undefined") {
this.editForm["virtual-display-primary"] = true;
}
if (typeof this.editForm["scale-factor"] === "undefined") {
this.editForm["scale-factor"] = "100"
}
this.showEditForm = true; this.showEditForm = true;
}, },
showDeleteForm(id) { showDeleteForm(app) {
let resp = confirm( const resp = confirm(
"Are you sure to delete " + this.apps[id].name + "?" "Are you sure to delete " + app.name + "?"
); );
if (resp) { if (resp) {
fetch("/api/apps/" + id, { fetch("/api/apps/delete?uuid=" + app.uuid, {
credentials: 'include', credentials: 'include',
method: "DELETE" method: "POST"
}).then((r) => { }).then((r) => {
if (r.status == 200) document.location.reload(); if (r.status == 200) document.location.reload();
}); });
} }
}, },
addPrepCmd() { addPrepCmd() {
let template = { const template = {
do: "", do: "",
undo: "" undo: ""
}; };
if (this.platform === 'windows') { if (this.platform === 'windows') {
template = { ...template, elevated: false }; template.elevated = false;
} }
this.editForm["prep-cmd"].push(template); this.editForm["prep-cmd"].push(template);
@@ -618,6 +600,8 @@
}, },
save() { save() {
this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, ''); this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
delete this.editForm["launching"];
delete this.editForm["id"];
fetch("/api/apps", { fetch("/api/apps", {
credentials: 'include', credentials: 'include',
method: "POST", method: "POST",

View File

@@ -427,9 +427,11 @@
} }
}) })
.catch((e) => { .catch((e) => {
console.error(e) console.error(e);
location.reload(); setTimeout(() => {
return location.reload();
}, 1000);
return;
}); });
} }
}); });

View File

@@ -92,7 +92,9 @@
"virtual_display_primary": "Enforce Virtual Display Primary", "virtual_display_primary": "Enforce Virtual Display Primary",
"virtual_display_primary_desc": "Automatically set the virtual display as primary display when the app starts. Virtual display will always be set to primary when client requests to use virtual display. (Recommended to keep on)", "virtual_display_primary_desc": "Automatically set the virtual display as primary display when the app starts. Virtual display will always be set to primary when client requests to use virtual display. (Recommended to keep on)",
"resolution_scale_factor": "Resolution Scale Factor", "resolution_scale_factor": "Resolution Scale Factor",
"resolution_scale_factor_desc": "Scale the client requested resolution based on this factor. e.g. 2000x1000 with a factor of 120% will become 2400x1200. Overrides client requested factor when the number isn't 100%. This option won't affect client requested streaming resolution." "resolution_scale_factor_desc": "Scale the client requested resolution based on this factor. e.g. 2000x1000 with a factor of 120% will become 2400x1200. Overrides client requested factor when the number isn't 100%. This option won't affect client requested streaming resolution.",
"use_app_identity": "Use App Identity",
"use_app_identity_desc": "Use the app's own identity while creating virtual displays instead of client's. This is useful when you want display configuration for each APP separately."
}, },
"client_card": { "client_card": {
"clients": "Clients", "clients": "Clients",

View File

@@ -86,11 +86,13 @@
"working_dir": "工作目录", "working_dir": "工作目录",
"working_dir_desc": "应传递给进程的工作目录。例如某些应用程序使用工作目录搜索配置文件。如果不设置Apollo 将默认使用命令的父目录", "working_dir_desc": "应传递给进程的工作目录。例如某些应用程序使用工作目录搜索配置文件。如果不设置Apollo 将默认使用命令的父目录",
"virtual_display": "总是使用虚拟显示器", "virtual_display": "总是使用虚拟显示器",
"virtual_display_desc": "在使用这个App的时候总是使用虚拟显示器覆盖客户端请求。请确保 SudoVDA 虚拟显示器驱动已安装并启用。", "virtual_display_desc": "在使用这个 App 的时候总是使用虚拟显示器,覆盖客户端请求。请确保 SudoVDA 虚拟显示器驱动已安装并启用。",
"virtual_display_primary": "强制设置虚拟显示器为主显示器", "virtual_display_primary": "强制设置虚拟显示器为主显示器",
"virtual_display_primary_desc": "在App启动时强制将虚拟显示器设为主显示器。当客户端请求使用虚拟显示器时将无条件设为主显示器。覆盖全局设置推荐保持开启", "virtual_display_primary_desc": "在 App 启动时强制将虚拟显示器设为主显示器。当客户端请求使用虚拟显示器时将无条件设为主显示器。(覆盖全局设置,推荐保持开启)",
"resolution_scale_factor": "分辨率缩放比例", "resolution_scale_factor": "分辨率缩放比例",
"resolution_scale_factor_desc": "基于此比例缩放客户端请求的分辨率。例如 2000x1000 缩放 120% 将变成 2400x1200。当此项为非 100% 时覆盖客户端请求的缩放比例。此选项不会影响客户端请求的串流分辨率。" "resolution_scale_factor_desc": "基于此比例缩放客户端请求的分辨率。例如 2000x1000 缩放 120% 将变成 2400x1200。当此项为非 100% 时覆盖客户端请求的缩放比例。此选项不会影响客户端请求的串流分辨率。",
"use_app_identity": "使用 App 身份",
"use_app_identity_desc": "在创建虚拟显示器时使用 App 自身的身份,而非客户端的。这样可以针对 APP 进行单独的显示器组合配置。"
}, },
"client_card": { "client_card": {
"clients": "客户端", "clients": "客户端",

View File

@@ -189,15 +189,19 @@
}) })
.then((resp) => { .then((resp) => {
if (resp.status !== 200) { if (resp.status !== 200) {
location.reload(); setTimeout(() => {
return location.reload();
}, 1000);
return;
} }
}) })
.catch((e) => { .catch((e) => {
this.serverRestarting = false this.serverRestarting = false
console.error(e) console.error(e);
location.reload(); setTimeout(() => {
return location.reload();
}, 1000);
return;
}); });
}, },
quit() { quit() {