Files
Apollo/src_assets/common/assets/web/config.html
Yukino Song b2ec048333 Fix WebUI
2025-05-28 13:47:58 +08:00

524 lines
16 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",
"stream_audio": "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]));
}
});
});
if (this.platform === 'windows') {
this.global_prep_cmd = this.config.global_prep_cmd.map((i) => {
i.elevated = !!i.elevated
return i
});
this.server_cmd = this.config.server_cmd.map((i) => {
i.elevated = !!i.elevated
return i
});
} else {
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>