Sunshine/src_assets/common/assets/web/config.html
2024-06-19 00:09:23 +00:00

469 lines
14 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;
}
.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"
: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"
:resolutions="resolutions"
:fps="fps"
>
</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" @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'
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,
fps: [],
resolutions: [],
currentTab: "general",
global_prep_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": "[]",
"notify_pre_releases": "disabled",
},
},
{
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",
"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",
"adapter_name": "",
"output_name": "",
"resolutions": "[352x240,480x360,858x480,1280x720,1920x1080,2560x1080,2560x1440,3440x1440,1920x1200,3840x2160,3840x1600]",
"fps": "[10,30,60,90,120]",
"min_fps_factor": 1,
},
},
{
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: {
"channels": 1,
"fec_percentage": 20,
"qp": 28,
"min_threads": 2,
"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",
},
},
{
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: "sw",
name: "Software Encoder",
options: {
"sw_preset": "superfast",
"sw_tune": "zerolatency",
},
},
],
};
},
provide() {
return {
platform: computed(() => this.platform)
}
},
created() {
fetch("/api/config")
.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";
});
}
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";
});
}
// remove values we don't want in the config file
delete this.config.platform;
delete this.config.status;
delete this.config.version;
// TODO: let each tab's Component handle it's own data instead of doing it here
// Populate default values from tabs options
this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => {
if (this.config[optionKey] === undefined) {
this.config[optionKey] = tab.options[optionKey];
}
});
});
this.fps = JSON.parse(this.config.fps);
//Resolutions should be fixed because are not valid JSON
let res = this.config.resolutions.substring(
1,
this.config.resolutions.length - 1
);
let resolutions = [];
res.split(",").forEach((r) => resolutions.push(r.trim()));
this.resolutions = resolutions;
this.config.global_prep_cmd = this.config.global_prep_cmd || [];
this.global_prep_cmd = JSON.parse(this.config.global_prep_cmd);
});
},
methods: {
forceUpdate() {
this.$forceUpdate()
},
serialize() {
let nl = this.config === "windows" ? "\r\n" : "\n";
this.config.resolutions =
"[" +
nl +
" " +
this.resolutions.join("," + nl + " ") +
nl +
"]";
// remove quotes from values in fps
this.config.fps = JSON.stringify(this.fps).replace(/"/g, "");
this.config.global_prep_cmd = JSON.stringify(this.global_prep_cmd);
},
save() {
this.saved = false;
this.restarted = false;
this.serialize();
// create a temp copy of this.config to use for the post request
let config = JSON.parse(JSON.stringify(this.config))
// delete default values from this.config
this.tabs.forEach(tab => {
Object.keys(tab.options).forEach(optionKey => {
let delete_value = false
if (["resolutions", "fps", "global_prep_cmd"].includes(optionKey)) {
let config_value, default_value
if (optionKey === "resolutions") {
let regex = /([\d]+x[\d]+)/g
// Use a regular expression to find each value and replace it with a quoted version
config_value = JSON.parse(config[optionKey].replace(regex, '"$1"')).toString()
default_value = JSON.parse(tab.options[optionKey].replace(regex, '"$1"')).toString()
} else {
config_value = JSON.parse(config[optionKey])
default_value = JSON.parse(tab.options[optionKey])
}
if (config_value === default_value) {
delete_value = true
}
}
// todo: add proper type checking
if (String(config[optionKey]) === String(tab.options[optionKey])) {
delete_value = true
}
if (delete_value) {
delete config[optionKey]
}
});
});
return fetch("/api/config", {
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"
});
}
});
},
},
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>