Files
Apollo/src_assets/common/assets/web/config.html
thesystemcoder168 12f3ba2d4b Add the option to always have the virtual display be an isolated display - Windows only (#662)
* Isolated display - web and backend

Isolated display - web and backend

* Moved settings outside of the advanced display

* Update virtual_display.cpp with isolated setting for better maintenance

Update virtual_display.cpp with isolated setting for better maintenance. The isolated routine becomes an additional routine instead of a replacement for the original one.

* Changed source formatting, removed English in non-English languages, removed test code

Changed source formatting, removed English in non-English languages, removed test code

* change blank/unused lines in config.cpp

change blank/unused lines in config.cpp

* Change blank lines in DisplayDeviceOptions.vue

Change blank lines in DisplayDeviceOptions.vue

* Change line spacing in config.h

Change line spacing in config.h

* Changed line spacing in config.cpp

Changed line spacing in config.cpp

* Changed lines/line spacing in virtual_display.cpp

Changed lines/line spacing in virtual_display.cpp

* Fix spaceing on virtual_display.cpp

Fix spaceing on virtual_display.cpp

* Added check to not do anything if the virtual display is not active.

Added check to not do anything to the order of the displays if the isolated checkbox is set and if the virtual display is not active.

* Minor formatting fix w/ add Chinese locale

* Expose `matchDisplay` though not used for now

---------

Co-authored-by: Yukino Song <nutosservice@gmail.com>
2025-05-19 15:00:11 +08:00

511 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<%- header %>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.pre-wrap {
white-space: pre-wrap;
}
.buttons {
padding: 1em 0;
}
</style>
</head>
<body id="app" v-cloak>
<Navbar></Navbar>
<div class="container">
<h1 class="my-4">{{ $t('config.configuration') }}</h1>
<div class="form" v-if="config">
<!-- Header -->
<ul class="nav nav-tabs">
<li class="nav-item" v-for="tab in tabs" :key="tab.id">
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
@click="currentTab = tab.id">{{tab.name}}</a>
</li>
</ul>
<!-- General Tab -->
<general
v-if="currentTab === 'general'"
:config="config"
:global-prep-cmd="global_prep_cmd"
:server-cmd="server_cmd"
:platform="platform">
</general>
<!-- Input Tab -->
<inputs
v-if="currentTab === 'input'"
:config="config"
:platform="platform">
</inputs>
<!-- Audio/Video Tab -->
<audio-video
v-if="currentTab === 'av'"
:config="config"
:platform="platform"
:vdisplay="vdisplayStatus"
>
</audio-video>
<!-- Network Tab -->
<network
v-if="currentTab === 'network'"
:config="config"
:platform="platform">
</network>
<!-- Files Tab -->
<files
v-if="currentTab === 'files'"
:config="config"
:platform="platform">
</files>
<!-- Advanced Tab -->
<advanced
v-if="currentTab === 'advanced'"
:config="config"
:platform="platform">
</advanced>
<container-encoders
:current-tab="currentTab"
:config="config"
:platform="platform">
</container-encoders>
</div>
<!-- Save and Apply buttons -->
<div class="alert alert-success my-4" v-if="saved && !restarted">
<b>{{ $t('_common.success') }}</b> {{ $t('config.apply_note') }}
</div>
<div class="alert alert-success my-4" v-if="restarted">
<b>{{ $t('_common.success') }}</b> {{ $t('config.restart_note') }}
</div>
<div class="mb-3 buttons">
<button class="btn btn-primary" @click="save">{{ $t('_common.save') }}</button>
<button class="btn btn-success mx-2" @click="apply" v-if="saved && !restarted">{{ $t('_common.apply') }}</button>
</div>
</div>
</body>
<script type="module">
import { computed, createApp } from 'vue'
import { initApp } from './init'
import Navbar from './Navbar.vue'
import General from './configs/tabs/General.vue'
import Inputs from './configs/tabs/Inputs.vue'
import Network from './configs/tabs/Network.vue'
import Files from './configs/tabs/Files.vue'
import Advanced from './configs/tabs/Advanced.vue'
import AudioVideo from './configs/tabs/AudioVideo.vue'
import ContainerEncoders from './configs/tabs/ContainerEncoders.vue'
import {$tp, usePlatformI18n} from './platform-i18n'
let fallbackDisplayModeCache = "";
const app = createApp({
components: {
Navbar,
General,
Inputs,
Network,
Files,
Advanced,
// They will be accessible via audio-video, container-encoders only.
AudioVideo,
ContainerEncoders,
},
data() {
return {
platform: "",
saved: false,
restarted: false,
config: null,
currentTab: "general",
vdisplayStatus: "1",
global_prep_cmd: [],
server_cmd: [],
tabs: [ // TODO: Move the options to each Component instead, encapsulate.
{
id: "general",
name: "General",
options: {
"locale": "en",
"sunshine_name": "",
"min_log_level": 2,
"global_prep_cmd": [],
"server_cmd": [],
"notify_pre_releases": "disabled",
"hide_tray_controls": "disabled",
"enable_pairing": "enabled",
"enable_discovery": "enabled",
},
},
{
id: "input",
name: "Input",
options: {
"controller": "enabled",
"gamepad": "auto",
"ds4_back_as_touchpad_click": "enabled",
"motion_as_ds4": "enabled",
"touchpad_as_ds4": "enabled",
"back_button_timeout": -1,
"keyboard": "enabled",
"key_repeat_delay": 500,
"key_repeat_frequency": 24.9,
"always_send_scancodes": "enabled",
"key_rightalt_to_key_win": "disabled",
"mouse": "enabled",
"high_resolution_scrolling": "enabled",
"native_pen_touch": "enabled",
"enable_input_only_mode": "disabled",
"keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI
},
},
{
id: "av",
name: "Audio/Video",
options: {
"audio_sink": "",
"virtual_sink": "",
"install_steam_audio_drivers": "enabled",
"keep_sink_default": "enabled",
"auto_capture_sink": "enabled",
"adapter_name": "",
"output_name": "",
"fallback_mode": "",
"dd_configuration_option": "disabled",
"dd_resolution_option": "auto",
"dd_manual_resolution": "",
"dd_refresh_rate_option": "auto",
"dd_manual_refresh_rate": "",
"dd_hdr_option": "auto",
"dd_config_revert_delay": 3000,
"dd_config_revert_on_disconnect": "disabled",
"dd_mode_remapping": {"mixed": [], "resolution_only": [], "refresh_rate_only": []},
"dd_wa_hdr_toggle": "disabled",
"fallback_mode": "",
"headless_mode": "disabled",
"double_refreshrate": "disabled",
"dd_wa_hdr_toggle_delay": 0,
"min_fps_factor": 1,
"max_bitrate": 0,
"isolated_virtual_display_option": "disabled",
},
},
{
id: "network",
name: "Network",
options: {
"upnp": "disabled",
"address_family": "ipv4",
"port": 47989,
"origin_web_ui_allowed": "lan",
"external_ip": "",
"lan_encryption_mode": 0,
"wan_encryption_mode": 1,
"ping_timeout": 10000,
},
},
{
id: "files",
name: "Config Files",
options: {
"file_apps": "",
"credentials_file": "",
"log_path": "",
"pkey": "",
"cert": "",
"file_state": "",
},
},
{
id: "advanced",
name: "Advanced",
options: {
"fec_percentage": 20,
"qp": 28,
"min_threads": 2,
"limit_framerate": "enabled",
"hevc_mode": 0,
"av1_mode": 0,
"capture": "",
"encoder": "",
},
},
{
id: "nv",
name: "NVIDIA NVENC Encoder",
options: {
"nvenc_preset": 1,
"nvenc_twopass": "quarter_res",
"nvenc_spatial_aq": "disabled",
"nvenc_vbv_increase": 0,
"nvenc_realtime_hags": "enabled",
"nvenc_latency_over_power": "enabled",
"nvenc_opengl_vulkan_on_dxgi": "enabled",
"nvenc_h264_cavlc": "disabled",
"nvenc_intra_refresh": "disabled"
},
},
{
id: "qsv",
name: "Intel QuickSync Encoder",
options: {
"qsv_preset": "medium",
"qsv_coder": "auto",
"qsv_slow_hevc": "disabled",
},
},
{
id: "amd",
name: "AMD AMF Encoder",
options: {
"amd_usage": "ultralowlatency",
"amd_rc": "vbr_latency",
"amd_enforce_hrd": "disabled",
"amd_quality": "balanced",
"amd_preanalysis": "disabled",
"amd_vbaq": "enabled",
"amd_coder": "auto",
},
},
{
id: "vt",
name: "VideoToolbox Encoder",
options: {
"vt_coder": "auto",
"vt_software": "auto",
"vt_realtime": "enabled",
},
},
{
id: "vaapi",
name: "VA-API Encoder",
options: {
"vaapi_strict_rc_buffer": "disabled",
},
},
{
id: "sw",
name: "Software Encoder",
options: {
"sw_preset": "superfast",
"sw_tune": "zerolatency",
},
},
],
};
},
provide() {
return {
platform: computed(() => this.platform)
}
},
created() {
fetch("./api/config", {
credentials: 'include'
})
.then((r) => r.json())
.then((r) => {
this.config = r;
this.platform = this.config.platform;
var app = document.getElementById("app");
if (this.platform === "windows") {
this.tabs = this.tabs.filter((el) => {
return el.id !== "vt" && el.id !== "vaapi";
});
}
if (this.platform === "linux") {
this.tabs = this.tabs.filter((el) => {
return el.id !== "amd" && el.id !== "qsv" && el.id !== "vt";
});
}
if (this.platform === "macos") {
this.tabs = this.tabs.filter((el) => {
return el.id !== "amd" && el.id !== "nv" && el.id !== "qsv" && el.id !== "vaapi";
});
}
// remove values we don't want in the config file
delete this.config.platform;
delete this.config.status;
delete this.config.version;
this.vdisplayStatus = this.config.vdisplayStatus;
delete this.config.vdisplayStatus;
fallbackDisplayModeCache = this.config.fallback_mode || "";
// TODO: let each tab's Component handle it's own data instead of doing it here
// Parse the special options before population if available
const specialOptions = ["dd_mode_remapping", "global_prep_cmd", "server_cmd"]
for (const optionKey of specialOptions) {
if (typeof this.config[optionKey] === "string") {
this.config[optionKey] = JSON.parse(this.config[optionKey]);
} else {
this.config[optionKey] = null;
}
}
this.config.dd_mode_remapping ??= {mixed: [], resolution_only: [], refresh_rate_only: []};
this.config.global_prep_cmd ??= [];
this.config.server_cmd ??= [];
// Populate default values from tabs options
this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => {
if (this.config[optionKey] === undefined) {
// Make sure to copy by value
this.config[optionKey] = JSON.parse(JSON.stringify(tab.options[optionKey]));
}
});
});
this.global_prep_cmd = this.config.global_prep_cmd;
this.server_cmd = this.config.server_cmd;
});
},
methods: {
forceUpdate() {
this.$forceUpdate()
},
serialize() {
// Validate fallback mode
if (this.config.fallback_mode && !this.config.fallback_mode.match(/^\d+x\d+x\d+$/)) {
this.config.fallback_mode = fallbackDisplayModeCache;
} else {
fallbackDisplayModeCache = this.config.fallback_mode;
}
let config = JSON.parse(JSON.stringify(this.config));
config.global_prep_cmd = this.global_prep_cmd.filter(cmd => cmd.do || cmd.undo);
config.dd_mode_remapping = config.dd_mode_remapping;
config.server_cmd = this.server_cmd.filter(cmd => cmd.name && cmd.cmd);
return config;
},
save() {
this.saved = false;
this.restarted = false;
// create a temp copy of this.config to use for the post request
let config = this.serialize();
// delete default values from this.config
this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => {
let delete_value = false
// todo: add proper type checking
if (JSON.stringify(config[optionKey]) === JSON.stringify(tab.options[optionKey])) {
delete_value = true
}
if (delete_value) {
delete config[optionKey]
}
});
});
return fetch("./api/config", {
credentials: 'include',
method: "POST",
body: JSON.stringify(config),
}).then((r) => {
if (r.status === 200) {
this.saved = true
return this.saved
}
else {
return false
}
});
},
apply() {
this.saved = this.restarted = false;
let saved = this.save();
saved.then((result) => {
if (result === true) {
this.restarted = true;
setTimeout(() => {
this.saved = this.restarted = false;
}, 5000);
fetch("./api/restart", {
method: "POST"
})
.then((resp) => {
if (resp.status !== 200) {
location.reload();
return
}
})
.catch((e) => {
console.error(e);
setTimeout(() => {
location.reload();
}, 1000);
return;
});
}
});
},
},
mounted() {
// Handle hashchange events
const handleHash = () => {
let hash = window.location.hash;
if (hash) {
// remove the # from the hash
let stripped_hash = hash.substring(1);
this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(key => {
if (tab.id === stripped_hash || key === stripped_hash) {
this.currentTab = tab.id;
}
if (key === stripped_hash) {
// sleep for 2 seconds to allow the page to load
setTimeout(() => {
let element = document.getElementById(stripped_hash);
if (element) {
window.location.hash = hash;
}
}, 2000);
}
if (this.currentTab === tab.id) {
// stop looping
return true;
}
});
});
}
};
// Call handleHash for the initial load
handleHash();
// Add hashchange event listener
window.addEventListener("hashchange", handleHash);
},
});
initApp(app);
</script>