Launch apps from web UI

This commit is contained in:
Yukino Song
2024-09-19 03:21:41 +08:00
parent af8b8fd352
commit 8e8f89d514
10 changed files with 223 additions and 101 deletions

View File

@@ -929,6 +929,66 @@ namespace confighttp {
} }
} }
void launchApp(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
auto args = request->parse_query_string();
if (
args.find("id"s) == std::end(args)
) {
outputTree.put("status", false);
outputTree.put("error", "Missing a required launch parameter");
return;
}
auto idx_str = nvhttp::get_arg(args, "id");
auto idx = util::from_view(idx_str);
const auto& apps = proc::proc.get_apps();
if (idx >= apps.size()) {
BOOST_LOG(error) << "Couldn't find app with index ["sv << idx_str << ']';
outputTree.put("status", false);
outputTree.put("error", "Cannot find requested application");
return;
}
auto& app = apps[idx];
auto appid = util::from_view(app.id);
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
disconnect(resp_https_t response, req_https_t request) { disconnect(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; if (!authenticate(response, request)) return;
@@ -945,7 +1005,7 @@ namespace confighttp {
pt::write_json(data, outputTree); pt::write_json(data, outputTree);
response->write(data.str()); response->write(data.str());
}); });
try { try {
pt::read_json(ss, inputTree); pt::read_json(ss, inputTree);
std::string uuid = inputTree.get<std::string>("uuid"); std::string uuid = inputTree.get<std::string>("uuid");
@@ -1033,6 +1093,7 @@ namespace confighttp {
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/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;

View File

@@ -180,7 +180,6 @@ namespace nvhttp {
client_t client_root; client_t client_root;
std::atomic<uint32_t> session_id_counter; std::atomic<uint32_t> session_id_counter;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>; using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>; using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>; using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
@@ -192,7 +191,7 @@ namespace nvhttp {
}; };
std::string std::string
get_arg(const args_t &args, const char *name, const char *default_value = nullptr) { get_arg(const args_t &args, const char *name, const char *default_value) {
auto it = args.find(name); auto it = args.find(name);
if (it == std::end(args)) { if (it == std::end(args)) {
if (default_value != NULL) { if (default_value != NULL) {
@@ -346,30 +345,58 @@ namespace nvhttp {
} }
std::shared_ptr<rtsp_stream::launch_session_t> std::shared_ptr<rtsp_stream::launch_session_t>
make_launch_session(bool host_audio, const args_t &args, const crypto::named_cert_t* named_cert_p) { make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p) {
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>(); auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
launch_session->id = ++session_id_counter; launch_session->id = ++session_id_counter;
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); // If launched from client
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); if (named_cert_p->uuid != http::unique_id) {
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
launch_session->host_audio = host_audio; launch_session->host_audio = host_audio;
std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0"));
// Split mode by the char "x", to populate width/height/fps // Split mode by the char "x", to populate width/height/fps
int x = 0; int x = 0;
std::string segment; std::string segment;
while (std::getline(mode, segment, 'x')) { while (std::getline(mode, segment, 'x')) {
if (x == 0) launch_session->width = atoi(segment.c_str()); if (x == 0) launch_session->width = atoi(segment.c_str());
if (x == 1) launch_session->height = atoi(segment.c_str()); if (x == 1) launch_session->height = atoi(segment.c_str());
if (x == 2) launch_session->fps = atoi(segment.c_str()); if (x == 2) launch_session->fps = atoi(segment.c_str());
x++; x++;
}
// Encrypted RTSP is enabled with client reported corever >= 1
auto corever = util::from_view(get_arg(args, "corever", "0"));
if (corever >= 1) {
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
launch_session->gcm_key, false
};
launch_session->rtsp_iv_counter = 0;
}
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
unsigned char raw_payload[8];
RAND_bytes(raw_payload, sizeof(raw_payload));
launch_session->av_ping_payload = util::hex_vec(raw_payload);
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
launch_session->iv.resize(16);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
auto prepend_iv_p = (uint8_t *) &prepend_iv;
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
} else {
launch_session->width = 0;
launch_session->height = 0;
launch_session->fps = 0;
} }
launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name; launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name;
launch_session->unique_id = named_cert_p->uuid; launch_session->unique_id = named_cert_p->uuid;
launch_session->perm = named_cert_p->perm; launch_session->perm = named_cert_p->perm;
launch_session->appid = util::from_view(get_arg(args, "appid", "unknown")); launch_session->appid = appid;
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
launch_session->surround_params = (get_arg(args, "surroundParams", "")); launch_session->surround_params = (get_arg(args, "surroundParams", ""));
@@ -378,26 +405,6 @@ namespace nvhttp {
launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0")); launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0"));
launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100")); launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100"));
// Encrypted RTSP is enabled with client reported corever >= 1
auto corever = util::from_view(get_arg(args, "corever", "0"));
if (corever >= 1) {
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
launch_session->gcm_key, false
};
launch_session->rtsp_iv_counter = 0;
}
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
unsigned char raw_payload[8];
RAND_bytes(raw_payload, sizeof(raw_payload));
launch_session->av_ping_payload = util::hex_vec(raw_payload);
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
launch_session->iv.resize(16);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
auto prepend_iv_p = (uint8_t *) &prepend_iv;
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
return launch_session; return launch_session;
} }
@@ -1032,8 +1039,11 @@ namespace nvhttp {
return; return;
} }
auto appid_str = get_arg(args, "appid");
auto appid = util::from_view(appid_str);
host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
auto launch_session = make_launch_session(host_audio, args, named_cert_p); auto launch_session = make_launch_session(host_audio, appid, args, named_cert_p);
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
@@ -1046,9 +1056,6 @@ namespace nvhttp {
return; return;
} }
auto appid = util::from_view(get_arg(args, "appid"));
auto appid_str = std::to_string(appid);
if (appid > 0) { if (appid > 0) {
const auto& apps = proc::proc.get_apps(); const auto& apps = proc::proc.get_apps();
auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) { auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) {
@@ -1161,7 +1168,7 @@ namespace nvhttp {
} }
} }
auto launch_session = make_launch_session(host_audio, args, named_cert_p); auto launch_session = make_launch_session(host_audio, 0, args, named_cert_p);
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {

View File

@@ -10,6 +10,7 @@
#include <chrono> #include <chrono>
// lib includes // lib includes
#include <Simple-Web-Server/server_https.hpp>
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
// local includes // local includes
@@ -24,6 +25,8 @@ using namespace std::chrono_literals;
*/ */
namespace nvhttp { namespace nvhttp {
using args_t = SimpleWeb::CaseInsensitiveMultimap;
/** /**
* @brief The protocol version. * @brief The protocol version.
* @details The version of the GameStream protocol we are mocking. * @details The version of the GameStream protocol we are mocking.
@@ -57,6 +60,12 @@ namespace nvhttp {
void void
start(); start();
std::string
get_arg(const args_t &args, const char *name, const char *default_value = nullptr);
std::shared_ptr<rtsp_stream::launch_session_t>
make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p);
/** /**
* @brief Compare the user supplied pin to the Moonlight pin. * @brief Compare the user supplied pin to the Moonlight pin.
* @param pin The user supplied pin. * @param pin The user supplied pin.

View File

@@ -89,6 +89,7 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) {
result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL); result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL);
if (result != DISP_CHANGE_SUCCESSFUL) { if (result != DISP_CHANGE_SUCCESSFUL) {
wprintf(L"[SUDOVDA] Changing config for display %ls failed!\n\n", displayDevice.DeviceName);
return false; return false;
} }
} }
@@ -98,10 +99,17 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) {
primaryDevMode.dmPosition.x = 0; primaryDevMode.dmPosition.x = 0;
primaryDevMode.dmPosition.y = 0; primaryDevMode.dmPosition.y = 0;
primaryDevMode.dmFields = DM_POSITION; primaryDevMode.dmFields = DM_POSITION;
ChangeDisplaySettingsExW(primaryDeviceName, &primaryDevMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET | CDS_SET_PRIMARY, NULL); result = ChangeDisplaySettingsExW(primaryDeviceName, &primaryDevMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET | CDS_SET_PRIMARY, NULL);
if (result != DISP_CHANGE_SUCCESSFUL) {
wprintf(L"[SUDOVDA] Changing config for primary display %ls failed!\n\n", primaryDeviceName);
return false;
}
wprintf(L"[SUDOVDA] Applying primary display %ls ...\n\n", primaryDeviceName);
result = ChangeDisplaySettingsExW(NULL, NULL, NULL, 0, NULL); result = ChangeDisplaySettingsExW(NULL, NULL, NULL, 0, NULL);
if (result != DISP_CHANGE_SUCCESSFUL) { if (result != DISP_CHANGE_SUCCESSFUL) {
wprintf(L"[SUDOVDA] Applying display coinfig failed!\n\n");
return false; return false;
} }

View File

@@ -170,8 +170,8 @@ namespace proc {
_app_id = app_id; _app_id = app_id;
_launch_session = launch_session; _launch_session = launch_session;
uint32_t client_width = launch_session->width; uint32_t client_width = launch_session->width ? launch_session->width : 1920;
uint32_t client_height = launch_session->height; uint32_t client_height = launch_session->height ? launch_session->height : 1080;
uint32_t render_width = client_width; uint32_t render_width = client_width;
uint32_t render_height = client_height; uint32_t render_height = client_height;
@@ -215,7 +215,7 @@ namespace proc {
launch_session->device_name.c_str(), launch_session->device_name.c_str(),
render_width, render_width,
render_height, render_height,
launch_session->fps, launch_session->fps ? launch_session->fps : 60,
launch_session->display_guid launch_session->display_guid
); );
@@ -223,8 +223,11 @@ namespace proc {
std::wstring currentPrimaryDisplayName = VDISPLAY::getPrimaryDisplay(); std::wstring currentPrimaryDisplayName = VDISPLAY::getPrimaryDisplay();
// Apply display settings // When launched through config ui, don't change display settings
VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, launch_session->fps); if (launch_session->width && launch_session->height && launch_session->fps) {
// Apply display settings
VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, launch_session->fps);
}
// Determine if we need to set the virtual display as primary // Determine if we need to set the virtual display as primary
bool shouldSetPrimary = false; bool shouldSetPrimary = false;
@@ -237,11 +240,12 @@ namespace proc {
// Set primary display if needed // Set primary display if needed
if (shouldSetPrimary) { if (shouldSetPrimary) {
VDISPLAY::setPrimaryDisplay( auto disp = (launch_session->virtual_display || _app.virtual_display_primary)
(launch_session->virtual_display || _app.virtual_display_primary) ? vdisplayName
? vdisplayName.c_str() : prevPrimaryDisplayName;
: prevPrimaryDisplayName.c_str() BOOST_LOG(info) << "Setting display " << disp << " primary!!!";
);
VDISPLAY::setPrimaryDisplay(disp.c_str());
} }
// Set virtual_display to true when everything went fine // Set virtual_display to true when everything went fine
@@ -373,7 +377,7 @@ namespace proc {
#ifdef _WIN32 #ifdef _WIN32
auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{ auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{
// Windows doesn't seem to be able to set HDR correctly when a display is just connected, // Windows doesn't seem to be able to set HDR correctly when a display is just connected / changed resolution,
// so we have tooggle HDR for the virtual display manually after a delay. // so we have tooggle HDR for the virtual display manually after a delay.
auto retryInterval = 200ms; auto retryInterval = 200ms;
while (is_changing_settings_going_to_fail()) { while (is_changing_settings_going_to_fail()) {
@@ -384,6 +388,7 @@ namespace proc {
std::this_thread::sleep_for(retryInterval); std::this_thread::sleep_for(retryInterval);
retryInterval *= 2; retryInterval *= 2;
} }
// We should have got the actual streaming display by now // We should have got the actual streaming display by now
std::string currentDisplay = this->display_name; std::string currentDisplay = this->display_name;
if (!currentDisplay.empty()) { if (!currentDisplay.empty()) {
@@ -419,6 +424,10 @@ namespace proc {
fg.disable(); fg.disable();
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_playing(_app.name);
#endif
return 0; return 0;
} }
@@ -778,36 +787,6 @@ namespace proc {
std::set<std::string> ids; std::set<std::string> ids;
std::vector<proc::ctx_t> apps; std::vector<proc::ctx_t> apps;
int i = 0; int i = 0;
#ifdef _WIN32
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
proc::ctx_t ctx;
ctx.name = "Virtual Display";
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
ctx.virtual_display = true;
ctx.virtual_display_primary = true;
ctx.scale_factor = 100;
ctx.elevated = false;
ctx.auto_detach = true;
ctx.wait_all = true;
ctx.exit_timeout = 5s;
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
if (ids.count(std::get<0>(possible_ids)) == 0) {
// Avoid using index to generate id if possible
ctx.id = std::get<0>(possible_ids);
}
else {
// Fallback to include index on collision
ctx.id = std::get<1>(possible_ids);
}
ids.insert(ctx.id);
apps.emplace_back(std::move(ctx));
}
#endif
for (auto &[_, app_node] : apps_node) { for (auto &[_, app_node] : apps_node) {
proc::ctx_t ctx; proc::ctx_t ctx;
@@ -915,6 +894,35 @@ namespace proc {
apps.emplace_back(std::move(ctx)); apps.emplace_back(std::move(ctx));
} }
#ifdef _WIN32
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
proc::ctx_t ctx;
ctx.name = "Virtual Display";
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
ctx.virtual_display = true;
ctx.virtual_display_primary = true;
ctx.scale_factor = 100;
ctx.elevated = false;
ctx.auto_detach = true;
ctx.wait_all = true;
ctx.exit_timeout = 5s;
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
if (ids.count(std::get<0>(possible_ids)) == 0) {
// Avoid using index to generate id if possible
ctx.id = std::get<0>(possible_ids);
}
else {
// Fallback to include index on collision
ctx.id = std::get<1>(possible_ids);
}
ids.insert(ctx.id);
apps.emplace_back(std::move(ctx));
}
#endif
return proc::proc_t { return proc::proc_t {
std::move(this_env), std::move(apps) std::move(this_env), std::move(apps)
}; };

View File

@@ -2123,9 +2123,6 @@ namespace stream {
// If this is the first session, invoke the platform callbacks // If this is the first session, invoke the platform callbacks
if (++running_sessions == 1) { if (++running_sessions == 1) {
platf::streaming_will_start(); platf::streaming_will_start();
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_playing(proc::proc.get_last_run_app_name());
#endif
} }
return 0; return 0;

View File

@@ -262,10 +262,10 @@ namespace system_tray {
tray_update(&tray); tray_update(&tray);
tray.icon = TRAY_ICON_PLAYING; tray.icon = TRAY_ICON_PLAYING;
tray.notification_title = "Stream Started"; tray.notification_title = "App launched";
char msg[256]; char msg[256];
static char force_close_msg[256]; static char force_close_msg[256];
snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); snprintf(msg, std::size(msg), "%s launched.", app_name.c_str());
snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str()); snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str());
#ifdef _WIN32 #ifdef _WIN32
strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
@@ -273,7 +273,7 @@ namespace system_tray {
#endif #endif
tray.notification_text = msg; tray.notification_text = msg;
tray.notification_icon = TRAY_ICON_PLAYING; tray.notification_icon = TRAY_ICON_PLAYING;
tray.tooltip = msg; tray.tooltip = PROJECT_NAME;
tray.menu[2].text = force_close_msg; tray.menu[2].text = force_close_msg;
tray_update(&tray); tray_update(&tray);
} }
@@ -299,7 +299,7 @@ namespace system_tray {
tray.notification_title = "Stream Paused"; tray.notification_title = "Stream Paused";
tray.notification_text = msg; tray.notification_text = msg;
tray.notification_icon = TRAY_ICON_PAUSING; tray.notification_icon = TRAY_ICON_PAUSING;
tray.tooltip = msg; tray.tooltip = PROJECT_NAME;
tray_update(&tray); tray_update(&tray);
} }

View File

@@ -89,10 +89,13 @@
<tr v-for="(app,i) in apps" :key="i"> <tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td> <td>{{app.name}}</td>
<td> <td>
<button class="btn btn-primary mx-1" @click="editApp(i)"> <button class="btn btn-success me-2" :disabled="app.launching" @click="launchApp(i)">
<i class="fas fa-play"></i> {{ $t('apps.launch') }}
</button>
<button class="btn btn-primary me-2" :disabled="app.launching" @click="editApp(i)">
<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 mx-1" @click="showDeleteForm(i)"> <button class="btn btn-danger" :disabled="app.launching" @click="showDeleteForm(i)">
<i class="fas fa-trash"></i> {{ $t('apps.delete') }} <i class="fas fa-trash"></i> {{ $t('apps.delete') }}
</button> </button>
</td> </td>
@@ -407,14 +410,7 @@
}; };
}, },
created() { created() {
fetch("/api/apps", { this.loadApps();
credentials: 'include'
})
.then((r) => r.json())
.then((r) => {
console.log(r);
this.apps = r.apps;
});
fetch("/api/config", { fetch("/api/config", {
credentials: 'include' credentials: 'include'
@@ -423,6 +419,15 @@
.then(r => this.platform = r.platform); .then(r => this.platform = r.platform);
}, },
methods: { methods: {
loadApps() {
fetch("/api/apps", {
credentials: 'include'
})
.then(r => r.json())
.then(r => {
this.apps = r.apps.map(i => ({...i, launching: false}));
});
},
newApp() { newApp() {
this.editForm = { this.editForm = {
name: "", name: "",
@@ -443,6 +448,25 @@
this.editForm.index = -1; this.editForm.index = -1;
this.showEditForm = true; this.showEditForm = true;
}, },
launchApp(id) {
const app = this.apps[id];
if (confirm(this.$t('apps.launch_warning'))) {
app.launching = true;
fetch("/api/apps/launch?id=" + id, {
credentials: 'include',
method: "POST",
})
.then(r => r.json())
.then(r => {
if (r.status == "true") {
alert(this.$t('apps.launch_success'));
} else {
alert(this.$t('apps.launch_failed') + r.error);
}
})
.finally(() => app.launching = false);
}
},
editApp(id) { editApp(id) {
this.editForm = JSON.parse(JSON.stringify(this.apps[id])); this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
this.editForm.index = id; this.editForm.index = id;

View File

@@ -74,6 +74,10 @@
"global_prep_name": "Global Prep Commands", "global_prep_name": "Global Prep Commands",
"image": "Image", "image": "Image",
"image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.", "image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.",
"launch": "Launch",
"launch_warning": "Are you sure you want to launch this app? This will terminate the currently running app.",
"launch_success": "App launched successfully!",
"launch_failed": "App launch failed: ",
"loading": "Loading...", "loading": "Loading...",
"name": "Name", "name": "Name",
"output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored", "output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored",

View File

@@ -72,6 +72,10 @@
"global_prep_name": "全局预处理命令", "global_prep_name": "全局预处理命令",
"image": "图片", "image": "图片",
"image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置Apollo 将发送默认图片。", "image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置Apollo 将发送默认图片。",
"launch": "启动",
"launch_warning": "确定要启动此应用吗?这将会终止当前已启动的应用。",
"launch_success": "应用启动成功!",
"launch_failed": "应用启动失败:",
"loading": "加载中...", "loading": "加载中...",
"name": "名称", "name": "名称",
"output_desc": "存储命令输出的文件,如果未指定,输出将被忽略", "output_desc": "存储命令输出的文件,如果未指定,输出将被忽略",