Web UI migration to Vite and Vue3 and improvements to the UX (#1673)

Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
This commit is contained in:
TheElixZammuto 2023-12-28 01:25:49 +01:00 committed by GitHub
parent 6b7b5996cc
commit 5bdbda90b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1868 additions and 2177 deletions

View File

@ -398,8 +398,6 @@ jobs:
mkdir -p build
mkdir -p artifacts
npm install
cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
@ -527,8 +525,6 @@ jobs:
BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }}
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
npm install
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release \
@ -719,8 +715,9 @@ jobs:
mingw-w64-x86_64-boost
mingw-w64-x86_64-cmake
mingw-w64-x86_64-curl
mingw-w64-x86_64-onevpl
mingw-w64-x86_64-nodejs
mingw-w64-x86_64-nsis
mingw-w64-x86_64-onevpl
mingw-w64-x86_64-openssl
mingw-w64-x86_64-opus
mingw-w64-x86_64-toolchain
@ -728,10 +725,6 @@ jobs:
wget
yasm
- name: Install npm packages
run: |
npm install
- name: Build Windows
shell: msys2 {0}
env:

View File

@ -84,3 +84,4 @@ elseif(UNIX)
include(${CMAKE_MODULE_PATH}/dependencies/linux.cmake)
endif()
endif()

View File

@ -12,9 +12,14 @@ set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png)
set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_STRIP_FILES YES)
# install npm modules
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/node_modules"
DESTINATION "${SUNSHINE_ASSETS_DIR}/web")
#install common assets
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}"
PATTERN "web" EXCLUDE)
# install built vite assets
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets/web"
DESTINATION "${SUNSHINE_ASSETS_DIR}")
# platform specific packaging
if(WIN32)

View File

@ -70,11 +70,11 @@ if(${SUNSHINE_TRAY} STREQUAL 1)
install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status"
RENAME "sunshine-tray.svg")
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-playing.svg"
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status")
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-pausing.svg"
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status")
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/images/sunshine-locked.svg"
install(FILES "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "\

View File

@ -10,8 +10,6 @@ if(SUNSHINE_PACKAGE_MACOS) # todo
set(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents")
set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS")
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}")
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}")

View File

@ -13,6 +13,3 @@ if(NOT CMAKE_INSTALL_PREFIX)
endif()
install(TARGETS sunshine RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}")

View File

@ -36,9 +36,6 @@ install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/"
COMPONENT gamepad)
# Sunshine assets
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}"
COMPONENT assets)
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}"
COMPONENT assets)

View File

@ -33,3 +33,9 @@ foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)
endforeach()
target_compile_options(sunshine PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>;$<$<COMPILE_LANGUAGE:CUDA>:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>) # cmake-lint: disable=C0301
#WebUI build
add_custom_target(web-ui ALL
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Installing NPM Dependencies and Building the Web UI"
COMMAND bash -c \"npm install && SUNSHINE_SOURCE_ASSETS_DIR=${SUNSHINE_SOURCE_ASSETS_DIR} SUNSHINE_ASSETS_DIR=${CMAKE_BINARY_DIR} npm run build\") # cmake-lint: disable=C0301

View File

@ -95,9 +95,6 @@ _INSTALL_CUDA
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build

View File

@ -31,6 +31,7 @@ set -e
apt-get update -y
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake=3.18.* \
git \
libavdevice-dev \
@ -58,8 +59,6 @@ apt-get install -y --no-install-recommends \
libxfixes-dev \
libxrandr-dev \
libxtst-dev \
nodejs \
npm \
wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
@ -69,6 +68,17 @@ apt-get clean
rm -rf /var/lib/apt/lists/*
_DEPS
#Install Node
# hadolint ignore=SC1091
RUN <<_INSTALL_NODE
#!/bin/bash
set -e
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source "$HOME/.nvm/nvm.sh"
nvm install 20.9.0
nvm use 20.9.0
_INSTALL_NODE
# install cuda
WORKDIR /build/cuda
# versions: https://developer.nvidia.com/cuda-toolkit-archive
@ -95,16 +105,17 @@ _INSTALL_CUDA
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build
# cmake and cpack
# hadolint ignore=SC1091
RUN <<_MAKE
#!/bin/bash
set -e
#Set Node version
source "$HOME/.nvm/nvm.sh"
nvm use 20.9.0
cmake \
-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \
-DCMAKE_BUILD_TYPE=Release \

View File

@ -52,7 +52,7 @@ dnf -y install \
libXrandr-devel \
libXtst-devel \
mesa-libGL-devel \
nodejs-npm \
nodejs \
numactl-devel \
openssl-devel \
opus-devel \
@ -94,9 +94,6 @@ _DEPS
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build

View File

@ -52,7 +52,7 @@ dnf -y install \
libXrandr-devel \
libXtst-devel \
mesa-libGL-devel \
nodejs-npm \
nodejs \
numactl-devel \
openssl-devel \
opus-devel \
@ -94,9 +94,6 @@ _DEPS
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build

View File

@ -31,6 +31,7 @@ set -e
apt-get update -y
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
gcc-10=10.5.* \
g++-10=10.5.* \
git \
@ -59,8 +60,6 @@ apt-get install -y --no-install-recommends \
libxfixes-dev \
libxrandr-dev \
libxtst-dev \
nodejs \
npm \
wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
@ -70,6 +69,17 @@ apt-get clean
rm -rf /var/lib/apt/lists/*
_DEPS
#Install Node
# hadolint ignore=SC1091
RUN <<_INSTALL_NODE
#!/bin/bash
set -e
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source "$HOME/.nvm/nvm.sh"
nvm install 20.9.0
nvm use 20.9.0
_INSTALL_NODE
# Update gcc alias
# https://stackoverflow.com/a/70653945/11214013
RUN <<_GCC_ALIAS
@ -131,16 +141,17 @@ _INSTALL_CUDA
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build
# cmake and cpack
# hadolint ignore=SC1091
RUN <<_MAKE
#!/bin/bash
set -e
#Set Node version
source "$HOME/.nvm/nvm.sh"
nvm use 20.9.0
cmake \
-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \
-DCMAKE_BUILD_TYPE=Release \

View File

@ -32,6 +32,7 @@ apt-get update -y
apt-get install -y --no-install-recommends \
build-essential \
cmake=3.22.* \
ca-certificates \
git \
libayatana-appindicator3-dev \
libavdevice-dev \
@ -58,8 +59,6 @@ apt-get install -y --no-install-recommends \
libxfixes-dev \
libxrandr-dev \
libxtst-dev \
nodejs \
npm \
wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \
@ -69,6 +68,17 @@ apt-get clean
rm -rf /var/lib/apt/lists/*
_DEPS
#Install Node
# hadolint ignore=SC1091
RUN <<_INSTALL_NODE
#!/bin/bash
set -e
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source "$HOME/.nvm/nvm.sh"
nvm install 20.9.0
nvm use 20.9.0
_INSTALL_NODE
# install cuda
WORKDIR /build/cuda
# versions: https://developer.nvidia.com/cuda-toolkit-archive
@ -95,16 +105,18 @@ _INSTALL_CUDA
WORKDIR /build/sunshine/
COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory
WORKDIR /build/sunshine/build
# cmake and cpack
# hadolint ignore=SC1091
RUN <<_MAKE
#!/bin/bash
set -e
#Set Node version
source "$HOME/.nvm/nvm.sh"
nvm use 20.9.0
#Actually build
cmake \
-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \
-DCMAKE_BUILD_TYPE=Release \

View File

@ -192,13 +192,6 @@ If the version of CUDA available from your distro is not adequate, manually inst
./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm
rm ./cuda.run
npm dependencies
----------------
Install npm dependencies.
.. code-block:: bash
npm install
Build
-----
.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing.

View File

@ -24,13 +24,6 @@ Install Requirements
cd /usr/local/include
ln -s ../opt/openssl/include/openssl .
npm dependencies
----------------
Install npm dependencies.
.. code-block:: bash
npm install
Build
-----
.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing.

View File

@ -16,17 +16,8 @@ Install dependencies:
pacman -S base-devel cmake diffutils gcc git make mingw-w64-x86_64-binutils \
mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-curl \
mingw-w64-x86_64-onevpl mingw-w64-x86_64-openssl mingw-w64-x86_64-opus \
mingw-w64-x86_64-toolchain
npm dependencies
----------------
Install nodejs and npm. Downloads available `here <https://nodejs.org/en/download/>`__.
Install npm dependencies.
.. code-block:: bash
npm install
mingw-w64-x86_64-nodejs mingw-w64-x86_64-onevpl mingw-w64-x86_64-openssl \
mingw-w64-x86_64-opus mingw-w64-x86_64-toolchain
Build
-----

View File

@ -69,7 +69,7 @@ source_suffix = ['.rst', '.md']
# -- Options for HTML output -------------------------------------------------
# images
html_favicon = os.path.join(root_dir, 'src_assets', 'common', 'assets', 'web', 'images', 'sunshine.ico')
html_favicon = os.path.join(root_dir, 'src_assets', 'common', 'assets', 'web', 'public', 'images', 'sunshine.ico')
html_logo = os.path.join(root_dir, 'sunshine.png')
# Add any paths that contain custom static files (such as style sheets) here,

View File

@ -3,3 +3,23 @@ Contributing
Read our contribution guide in our organization level
`docs <https://lizardbyte.readthedocs.io/en/latest/developers/contributing.html>`__.
Web UI
------
The Web UI uses `Vite <https://vitejs.dev/>`__ as its build system, to handle the integration of the NPM libraries.
The HTML pages used by the Web UI are found in ``src_assets/common/assets/web``.
`EJS <https://www.npmjs.com/package/vite-plugin-ejs>`__ is used as a templating system for the pages (check ``template_header.html`` and ``template_header_main.html``).
The Style System is provided by `Bootstrap <https://getbootstrap.com/>`__.
The JS framework used by the more interactive pages is `Vue <https://vuejs.org/>`__.
Building
^^^^^^^^
Sunshine already builds the UI as part of its build process, but you can make faster changes by starting vite manually.
.. code-block:: bash
npm run dev

View File

@ -1,7 +1,15 @@
{
"scripts": {
"build": "vite build --debug",
"dev": "vite build --watch"
},
"dependencies": {
"@fortawesome/fontawesome-free": "6.4.2",
"@popperjs/core": "2.11.8",
"@vitejs/plugin-vue": "4.3.4",
"bootstrap": "5.3.2",
"vue": "2.6.12"
"vite": "4.4.9",
"vite-plugin-ejs": "1.6.4",
"vue": "3.2.25"
}
}

View File

@ -70,10 +70,6 @@ prepare() {
}
build() {
pushd "$pkgname"
npm install
popd
export BRANCH="@GITHUB_BRANCH@"
export BUILD_VERSION="@GITHUB_BUILD_VERSION@"
export COMMIT="@GITHUB_COMMIT@"

View File

@ -312,9 +312,6 @@ modules:
env:
npm_config_nodedir: /usr/lib/sdk/node18
NPM_CONFIG_LOGLEVEL: info
build-commands:
# Install npm dependencies
- cd ${FLATPAK_BUILDER_BUILDDIR} && npm install
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/app

View File

@ -55,10 +55,6 @@ platform darwin {
}
}
pre-build {
system -W ${worksrcpath} "npm install"
}
notes-append "Run @PROJECT_NAME@ by executing 'sunshine <path to user config>', e.g. 'sunshine ~/sunshine.conf' "
notes-append "The config file will be created if it doesn't exist."
notes-append "It is recommended to set a location for the apps file in the config."

View File

@ -37,9 +37,9 @@ icon_sizes=${!icon_sizes_keys[@]}
echo "using icon sizes:"
echo ${icon_sizes[@]}
src_vectors=("../../src_assets/common/assets/web/images/sunshine-locked.svg"
"../../src_assets/common/assets/web/images/sunshine-pausing.svg"
"../../src_assets/common/assets/web/images/sunshine-playing.svg"
src_vectors=("../../src_assets/common/assets/web/public/images/sunshine-locked.svg"
"../../src_assets/common/assets/web/public/images/sunshine-pausing.svg"
"../../src_assets/common/assets/web/public/images/sunshine-playing.svg"
"../../sunshine.svg")
echo "using sources vectors:"

View File

@ -161,11 +161,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "index.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -174,11 +173,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "pin.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -187,12 +185,11 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "apps.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -201,11 +198,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "clients.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -214,11 +210,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "config.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -227,11 +222,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "password.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -241,11 +235,10 @@ namespace confighttp {
send_redirect(response, request, "/");
return;
}
std::string header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -254,11 +247,10 @@ namespace confighttp {
print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "troubleshooting.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers);
response->write(content, headers);
}
void
@ -295,14 +287,14 @@ namespace confighttp {
getNodeModules(resp_https_t response, req_https_t request) {
print_req(request);
fs::path webDirPath(WEB_DIR);
fs::path nodeModulesPath(webDirPath / "node_modules");
fs::path nodeModulesPath(webDirPath / "assets");
// .relative_path is needed to shed any leading slash that might exist in the request path
auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path());
// Don't do anything if file does not exist or is outside the node_modules directory
// Don't do anything if file does not exist or is outside the assets directory
if (!isChildPath(filePath, nodeModulesPath)) {
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules folder";
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request");
}
else if (!fs::exists(filePath)) {
@ -757,7 +749,7 @@ namespace confighttp {
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
server.resource["^/node_modules\\/.+$"]["GET"] = getNodeModules;
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
server.config.reuse_address = true;
server.config.address = net::af_to_any_address_string(address_family);
server.config.port = port_https;

View File

@ -0,0 +1,60 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #ffc400">
<div class="container-fluid">
<a class="navbar-brand" href="/" title="Sunshine">
<img src="/images/logo-sunshine-45.png" height="45" alt="Sunshine">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-fw fa-home"></i> Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/pin"><i class="fas fa-fw fa-unlock"></i> PIN</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/apps"><i class="fas fa-fw fa-stream"></i> Applications</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config"><i class="fas fa-fw fa-cog"></i> Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/password"><i class="fas fa-fw fa-user-shield"></i> Change Password</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/troubleshooting"><i class="fas fa-fw fa-info"></i> Troubleshooting</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
created() {
console.log("Header mounted!")
},
mounted() {
let el = document.querySelector("a[href='" + document.location.pathname + "']");
if (el) el.classList.add("active")
let discordWidget = document.createElement('script')
discordWidget.setAttribute('src', 'https://app.lizardbyte.dev/js/discord.js')
document.head.appendChild(discordWidget)
}
}
</script>
<style>
.nav-link.active {
font-weight: 500;
}
.form-control::placeholder {
opacity: 0.5;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="card p-2">
<div class="card-body">
<h2>Resources</h2>
<br />
<p>
Resources for Sunshine!
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-success m-1" href="https://app.lizardbyte.dev" target="_blank">LizardByte Website</a>
<a class="btn btn-primary m-1" href="https://app.lizardbyte.dev/discord" target="_blank">
<i class="fab fa-fw fa-discord"></i> Discord</a>
<a class="btn btn-secondary m-1" href="https://github.com/LizardByte/Sunshine/discussions" target="_blank">
<i class="fab fa-fw fa-github"></i> Github Discussions</a>
</div>
</div>
</div>
<!--Legal-->
<div class="card p-2 mt-4">
<div class="card-body">
<h2>Legal</h2>
<br />
<p>
By continuing to use this software you agree to the terms and conditions in the following documents.
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/LICENSE"
target="_blank">
<i class="fas fa-fw fa-file-alt"></i> License</a>
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/NOTICE"
target="_blank">
<i class="fas fa-fw fa-exclamation"></i> Third Party Notice</a>
</div>
</div>
</div>
</template>

View File

@ -1,4 +1,78 @@
<div id="app" class="container">
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
<style>
.precmd-head {
width: 200px;
}
.monospace {
font-family: monospace;
}
.cover-finder {}
.cover-finder .cover-results {
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
.cover-finder .cover-results.busy * {
cursor: wait !important;
pointer-events: none;
}
.cover-container {
padding-top: 133.33%;
position: relative;
}
.cover-container.result {
cursor: pointer;
}
.spinner-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.cover-container img {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
td {
padding: 0 0.5em;
}
.env-table td {
padding: 0.25em;
border-bottom: rgba(0, 0, 0, 0.25) 1px solid;
vertical-align: top;
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<div class="my-4">
<h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div>
@ -15,10 +89,10 @@
<tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td>
<td>
<button class="btn btn-primary" @click="editApp(i)">
<button class="btn btn-primary mx-1" @click="editApp(i)">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-danger" @click="showDeleteForm(i)">
<button class="btn btn-danger mx-1" @click="showDeleteForm(i)">
<i class="fas fa-trash"></i> Delete
</button>
</td>
@ -31,13 +105,7 @@
<!--name-->
<div class="mb-3">
<label for="appName" class="form-label">Application Name</label>
<input
type="text"
class="form-control"
id="appName"
aria-describedby="appNameHelp"
v-model="editForm.name"
/>
<input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name" />
<div id="appNameHelp" class="form-text">
Application Name, as shown on Moonlight
</div>
@ -45,13 +113,8 @@
<!--output-->
<div class="mb-3">
<label for="appOutput" class="form-label">Output</label>
<input
type="text"
class="form-control monospace"
id="appOutput"
aria-describedby="appOutputHelp"
v-model="editForm.output"
/>
<input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
v-model="editForm.output" />
<div id="appOutputHelp" class="form-text">
The file where the output of the command is stored, if it is not
specified, the output is ignored
@ -59,14 +122,8 @@
</div>
<!--prep-cmd-->
<div class="mb-3">
<label for="excludeGlobalPrep" class="form-label"
>Global Prep Commands</label
>
<select
id="excludeGlobalPrep"
class="form-select"
v-model="editForm['exclude-global-prep-cmd']"
>
<label for="excludeGlobalPrep" class="form-label">Global Prep Commands</label>
<select id="excludeGlobalPrep" class="form-select" v-model="editForm['exclude-global-prep-cmd']">
<option v-for="val in [false, true]" :value="val">
{{ !val ? 'Enabled' : 'Disabled' }}
</option>
@ -82,10 +139,7 @@
A list of commands to be run before/after this application.<br />
If any of the prep-commands fail, starting the application is aborted.
</div>
<div
class="d-flex justify-content-start mb-3 mt-3"
v-if="editForm['prep-cmd'].length === 0"
>
<div class="d-flex justify-content-start mb-3 mt-3" v-if="editForm['prep-cmd'].length === 0">
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus mr-1"></i> Add Commands
</button>
@ -104,39 +158,20 @@
<tbody>
<tr v-for="(c, i) in editForm['prep-cmd']">
<td>
<input
type="text"
class="form-control monospace"
v-model="c.do"
/>
<input type="text" class="form-control monospace" v-model="c.do" />
</td>
<td>
<input
type="text"
class="form-control monospace"
v-model="c.undo"
/>
<input type="text" class="form-control monospace" v-model="c.undo" />
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="'prep-cmd-admin-' + i"
v-model="c.elevated"
true-value="true"
false-value="false"
/>
<label :for="'prep-cmd-admin-' + i" class="form-check-label"
>Elevated</label
>
<input type="checkbox" class="form-check-input" :id="'prep-cmd-admin-' + i" v-model="c.elevated"
true-value="true" false-value="false" />
<label :for="'prep-cmd-admin-' + i" class="form-check-label">Elevated</label>
</div>
</td>
<td>
<button
class="btn btn-danger"
@click="$delete(editForm['prep-cmd'], i)"
>
<button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="addPrepCmd">
@ -147,32 +182,18 @@
</tbody>
</table>
</div>
<!--detatched-->
<!--detached-->
<div class="mb-3">
<label for="appName" class="form-label">Detached Commands</label>
<div
v-for="(c,i) in editForm.detached"
class="d-flex justify-content-between my-2"
>
<pre>{{c}}</pre>
<button
class="btn btn-danger mx-2"
@click="editForm.detached.splice(i,1)"
>
<div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
<input type="text" v-model="editForm.detached[i]" class="form-control monospace">
<button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">
&times;
</button>
</div>
<div class="d-flex justify-content-between">
<input
type="text"
class="form-control monospace"
v-model="detachedCmd"
/>
<button
class="btn btn-success mx-2"
@click="editForm.detached.push(detachedCmd);detachedCmd = '';"
>
+
<button class="btn btn-success" @click="editForm.detached.push('');">
<i class="fas fa-plus mr-1"></i> Add Detached Command
</button>
</div>
<div class="form-text">
@ -182,13 +203,8 @@
<!--command-->
<div class="mb-3">
<label for="appCmd" class="form-label">Command</label>
<input
type="text"
class="form-control monospace"
id="appCmd"
aria-describedby="appCmdHelp"
v-model="editForm.cmd"
/>
<input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp"
v-model="editForm.cmd" />
<div id="appCmdHelp" class="form-text">
The main application, if it is not specified, a process is started
that sleeps indefinitely
@ -197,13 +213,8 @@
<!--working dir-->
<div class="mb-3">
<label for="appWorkingDir" class="form-label">Working Directory</label>
<input
type="text"
class="form-control monospace"
id="appWorkingDir"
aria-describedby="appWorkingDirHelp"
v-model="editForm['working-dir']"
/>
<input type="text" class="form-control monospace" id="appWorkingDir" aria-describedby="appWorkingDirHelp"
v-model="editForm['working-dir']" />
<div id="appWorkingDirHelp" class="form-text">
The working directory that should be passed to the process. For
example, some applications use the working directory to search for
@ -213,17 +224,9 @@
</div>
<!-- elevation -->
<div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="appElevation" class="form-check-label"
>Run as administrator</label
>
<input
type="checkbox"
class="form-check-input"
id="appElevation"
v-model="editForm.elevated"
true-value="true"
false-value="false"
/>
<label for="appElevation" class="form-check-label">Run as administrator</label>
<input type="checkbox" class="form-check-input" id="appElevation" v-model="editForm.elevated"
true-value="true" false-value="false" />
<div class="form-text">
This can be necessary for some applications that require administrator
permissions to run properly.
@ -231,80 +234,41 @@
</div>
<!-- auto-detach -->
<div class="mb-3 form-check">
<label for="autoDetach" class="form-check-label"
>Continue streaming if the application exits quickly</label
>
<input
type="checkbox"
class="form-check-input"
id="autoDetach"
v-model="editForm['auto-detach']"
true-value="true"
false-value="false"
/>
<label for="autoDetach" class="form-check-label">Continue streaming if the application exits quickly</label>
<input type="checkbox" class="form-check-input" id="autoDetach" v-model="editForm['auto-detach']"
true-value="true" false-value="false" />
<div class="form-text">
This will attempt to automatically detect launcher-type apps that close
quickly after launching another program or instance of themselves. When
a launcher-type app is detected, it is treated as a detached app.
</div>
</div>
<!-- Image path -->
<div class="mb-3">
<label for="appImagePath" class="form-label">Image</label>
<div class="input-group dropup">
<input
type="text"
class="form-control monospace"
id="appImagePath"
aria-describedby="appImagePathHelp"
v-model="editForm['image-path']"
/>
<button
class="btn btn-secondary dropdown-toggle"
type="button"
id="findCoverToggle"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
v-dropdown-show="showCoverFinder"
ref="coverFinderDropdown"
>
<input type="text" class="form-control monospace" id="appImagePath" aria-describedby="appImagePathHelp"
v-model="editForm['image-path']" />
<button class="btn btn-secondary dropdown-toggle" type="button" id="findCoverToggle"
aria-expanded="false" @click="showCoverFinder" ref="coverFinderDropdown">
Find Cover
</button>
<div
class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle"
>
<div class="modal-header">
<div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle">
<div class="modal-header px-2">
<h4 class="modal-title">Covers Found</h4>
<button
type="button"
class="btn-close"
aria-label="Close"
@click="closeCoverFinder"
></button>
<button type="button" class="btn-close mr-2" aria-label="Close" @click="closeCoverFinder"></button>
</div>
<div
class="modal-body cover-results px-3 pt-3"
:class="{ busy: coverFinderBusy }"
>
<div class="modal-body cover-results px-3 pt-3" :class="{ busy: coverFinderBusy }">
<div class="row">
<div
v-if="coverSearching"
class="col-12 col-sm-6 col-lg-4 mb-3"
>
<div v-if="coverSearching" class="col-12 col-sm-6 col-lg-4 mb-3">
<div class="cover-container">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div
v-for="(cover,i) in coverCandidates"
:key="'i'"
class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)"
>
<div v-for="(cover,i) in coverCandidates" :key="'i'" class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)">
<div class="cover-container result">
<img class="rounded" :src="cover.url" />
</div>
@ -321,25 +285,74 @@
must be a PNG file. If not set, Sunshine will send default box image.
</div>
</div>
<div class="env-hint">
<div class="form-text"><b>About Environment Variables: </b> All commands get these environment variables by default: </div>
<table>
<tr><td><b>Var Name</b></td><td><b></b></td></tr>
<tr><td style="font-family: monospace">SUNSHINE_APP_ID</td><td>App ID</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_APP_NAME</td><td>App Name</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td><td>The Width requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td><td>The Height requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td><td>The FPS requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td><td>(true/false) if HDR is enabled by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td><td>(int) the requested gamepad mask, in a bitset/bitfield format</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td><td>(true/false) if the client has requested host audio</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td><td>(true/false) if the client has requested the option to optimize the game for optimal streaming</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td><td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td></tr>
<div class="env-hint alert alert-info">
<div class="form-text">
<h4>About Environment Variables</h4>
All commands get these environment variables by default:
</div>
<table class="env-table">
<tr>
<td><b>Var Name</b></td>
<td><b></b></td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_ID</td>
<td>App ID</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_NAME</td>
<td>App Name</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td>
<td>The Width requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td>
<td>The Height requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td>
<td>The FPS requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td>
<td>(true/false) if HDR is enabled by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td>
<td>(int) the requested gamepad mask, in a bitset/bitfield format</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td>
<td>(true/false) if the client has requested host audio</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td>
<td>(true/false) if the client has requested the option to optimize the game for optimal
streaming</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td>
<td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td>
</tr>
</table>
<div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution Automation:</b> <pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre></div>
<div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution Automation:</b> <pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre></div>
<div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for Resolution Automation:</b> <pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre></div>
<div class="form-text"><a href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html" target="_blank">See More</a></div>
<div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution
Automation:</b>
<pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre>
</div>
<div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution
Automation:</b>
<pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre>
</div>
<div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for
Resolution
Automation:</b>
<pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre>
</div>
<div class="form-text"><a
href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html"
target="_blank">See More</a></div>
</div>
<!--buttons-->
<div class="d-flex">
@ -356,15 +369,15 @@
</button>
</div>
</div>
<script>
Vue.directive('dropdown-show', {
bind: function (el, binding) {
el.addEventListener('show.bs.dropdown', binding.value);
}
});
new Vue({
el: "#app",
</body>
<script type="module">
import { createApp } from 'vue';
import Navbar from './Navbar.vue'
import {Dropdown} from 'bootstrap'
const app = createApp({
components: {
Navbar
},
data() {
return {
apps: [],
@ -408,18 +421,18 @@
},
editApp(id) {
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
this.$set(this.editForm, "index", id);
this.editForm.index = id;
if (this.editForm["prep-cmd"] === undefined)
this.$set(this.editForm, "prep-cmd", []);
this.editForm["prep-cmd"] = [];
if (this.editForm["detached"] === undefined)
this.$set(this.editForm, "detached", []);
this.editForm["detached"] = [];
if (this.editForm["exclude-global-prep-cmd"] === undefined)
this.$set(this.editForm, "exclude-global-prep-cmd", false);
this.editForm["exclude-global-prep-cmd"] = [];
if (this.editForm["elevated"] === undefined && this.platform === 'windows') {
this.$set(this.editForm, "elevated", false);
this.editForm["elevated"] = [];
}
if (this.editForm["auto-detach"] === undefined) {
this.$set(this.editForm, "auto-detach", true);
this.editForm["auto-detach"] = true;
}
this.showEditForm = true;
},
@ -448,7 +461,19 @@
showCoverFinder($event) {
this.coverCandidates = [];
this.coverSearching = true;
const ref = this.$refs.coverFinderDropdown;
if (!ref) {
console.error("Ref not found!");
return;
}
this.coverFinderDropdown = Dropdown.getInstance(ref);
if (!this.coverFinderDropdown) {
this.coverFinderDropdown = new Dropdown(ref);
if (!this.coverFinderDropdown) {
return;
}
}
this.coverFinderDropdown.show();
function getSearchBucket(name) {
let bucket = name.substring(0, Math.min(name.length, 2)).toLowerCase().replaceAll(/[^a-z\d]/g, '');
if (!bucket) {
@ -503,7 +528,7 @@
if (!ref) {
return;
}
const dropdown = this.coverFinderDropdown = bootstrap.Dropdown.getInstance(ref);
const dropdown = this.coverFinderDropdown = Dropdown.getInstance(ref);
if (!dropdown) {
return;
}
@ -520,7 +545,7 @@
}).then(r => {
if (!r.ok) throw new Error("Failed to download covers");
return r.json();
}).then(body => this.$set(this.editForm, "image-path", body.path))
}).then(body => this.editForm["image-path"] = body.path)
.then(() => this.closeCoverFinder())
.finally(() => this.coverFinderBusy = false);
},
@ -535,66 +560,13 @@
},
},
});
app.directive('dropdown-show', {
mounted: function (el, binding) {
el.addEventListener('show.bs.dropdown', binding.value);
}
});
app.mount("#app")
</script>
<style>
.precmd-head {
width: 200px;
}
.monospace {
font-family: monospace;
}
.cover-finder {
}
.cover-finder .cover-results {
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
.cover-finder .cover-results.busy * {
cursor: wait !important;
pointer-events: none;
}
.cover-container {
padding-top: 133.33%;
position: relative;
}
.cover-container.result {
cursor: pointer;
}
.spinner-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.cover-container img {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
td {
padding: 0 0.5em;
}
</style>

View File

@ -1,3 +0,0 @@
<div id="content" class="container">
<h1>Clients</h1>
</div>

View File

@ -1,16 +1,37 @@
<div id="app" class="container">
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
.ms-item {
background-color: #ccc;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<h1 class="my-4">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
>
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
@click="currentTab = tab.id">{{tab.name}}</a>
</li>
</ul>
<!--General Tab-->
@ -18,13 +39,8 @@
<!--Sunshine Name-->
<div class="mb-3">
<label for="sunshine_name" class="form-label">Sunshine Name</label>
<input
type="text"
class="form-control"
id="sunshine_name"
placeholder="Sunshine"
v-model="config.sunshine_name"
/>
<input type="text" class="form-control" id="sunshine_name" placeholder="Sunshine"
v-model="config.sunshine_name" />
<div class="form-text">
The name displayed by Moonlight. If not specified, the PC's hostname is used
</div>
@ -32,11 +48,7 @@
<!--Log Level-->
<div class="mb-3">
<label for="min_log_level" class="form-label">Log Level</label>
<select
id="min_log_level"
class="form-select"
v-model="config.min_log_level"
>
<select id="min_log_level" class="form-select" v-model="config.min_log_level">
<option value="0">Verbose</option>
<option value="1">Debug</option>
<option value="2">Info</option>
@ -52,28 +64,15 @@
<!--Log Path-->
<div class="mb-3">
<label for="log_path" class="form-label">Logfile Path</label>
<input
type="text"
class="form-control"
id="log_path"
placeholder="sunshine.log"
v-model="config.log_path"
/>
<input type="text" class="form-control" id="log_path" placeholder="sunshine.log" v-model="config.log_path" />
<div class="form-text">
The file where the current logs of Sunshine are stored.
</div>
</div>
<!--Origin Web UI Allowed-->
<div class="mb-3">
<label for="origin_web_ui_allowed" class="form-label"
>Origin Web UI Allowed</label
>
<select
id="origin_web_ui_allowed"
class="form-select"
v-model="config.origin_web_ui_allowed"
@change="forceUpdate"
>
<label for="origin_web_ui_allowed" class="form-label">Origin Web UI Allowed</label>
<select id="origin_web_ui_allowed" class="form-select" v-model="config.origin_web_ui_allowed">
<option value="pc">Only localhost may access Web UI</option>
<option value="lan">Only those in LAN may access Web UI</option>
<option value="wan">Anyone may access Web UI</option>
@ -81,10 +80,6 @@
<div class="form-text">
The origin of the remote endpoint address that is not denied access to Web UI
</div>
<!-- add warning about exposing web ui to the internet -->
<div class="alert alert-danger" v-if="config.origin_web_ui_allowed === 'wan'">
<i class="fa-solid fa-xl fa-triangle-exclamation"></i> Exposing the Web UI to the internet is a security risk! Proceed at your own risk!
</div>
</div>
<!--UPnP-->
<div class="mb-3">
@ -118,7 +113,8 @@
<div class="accordion-body">
<div>
<label for="ds4_back_as_touchpad_click" class="form-label">Map Back/Select to Touchpad Click</label>
<select id="ds4_back_as_touchpad_click" class="form-select" v-model="config.ds4_back_as_touchpad_click">
<select id="ds4_back_as_touchpad_click" class="form-select"
v-model="config.ds4_back_as_touchpad_click">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled (default)</option>
</select>
@ -131,54 +127,27 @@
<!--Ping Timeout-->
<div class="mb-3">
<label for="ping_timeout" class="form-label">Ping Timeout</label>
<input
type="text"
class="form-control"
id="ping_timeout"
placeholder="10000"
v-model="config.ping_timeout"
/>
<input type="text" class="form-control" id="ping_timeout" placeholder="10000" v-model="config.ping_timeout" />
<div class="form-text">
How long to wait in milliseconds for data from moonlight before shutting down the stream
</div>
</div>
<!--Advertised FPS and Resolutions-->
<div class="mb-3">
<label for="ping_timeout" class="form-label"
>Advertised Resolutions and FPS</label
>
<label for="ping_timeout" class="form-label">Advertised Resolutions and FPS</label>
<div class="resolutions-container">
<label>Resolutions</label>
<div class="resolutions d-flex flex-wrap">
<div
class="p-2 ms-item m-2 d-flex justify-content-between"
v-for="(r,i) in resolutions"
:key="r"
>
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(r,i) in resolutions" :key="r">
<span class="px-2">{{r}}</span>
<span style="cursor: pointer" @click="resolutions.splice(i,1)"
>&times;</span
>
<span style="cursor: pointer" @click="resolutions.splice(i,1)">&times;</span>
</div>
<form
@submit.prevent="resolutions.push(resIn);resIn = '';"
class="d-flex align-items-center"
>
<input
type="text"
v-model="resIn"
required
pattern="[0-9]+x[0-9]+"
style="
<form @submit.prevent="resolutions.push(resIn);resIn = '';" class="d-flex align-items-center">
<input type="text" v-model="resIn" required pattern="[0-9]+x[0-9]+" style="
border-top-right-radius: 0;
border-bottom-right-radius: 0;
"
class="form-control"
/>
<button
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
class="btn btn-success"
>
" class="form-control" />
<button style="border-top-left-radius: 0; border-bottom-left-radius: 0" class="btn btn-success">
+
</button>
</form>
@ -187,36 +156,17 @@
<div class="fps-container">
<label>FPS</label>
<div class="fps d-flex flex-wrap">
<div
class="p-2 ms-item m-2 d-flex justify-content-between"
v-for="(f,i) in fps"
:key="f"
>
<div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(f,i) in fps" :key="f">
<span class="px-2">{{f}}</span>
<span style="cursor: pointer" @click="fps.splice(i,1)"
>&times;</span
>
<span style="cursor: pointer" @click="fps.splice(i,1)">&times;</span>
</div>
<form
@submit.prevent="fps.push(fpsIn);fpsIn = '';"
class="d-flex align-items-center"
>
<input
type="text"
v-model="fpsIn"
required
pattern="[0-9]+"
style="
<form @submit.prevent="fps.push(fpsIn);fpsIn = '';" class="d-flex align-items-center">
<input type="text" v-model="fpsIn" required pattern="[0-9]+" style="
width: 6ch;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
"
class="form-control"
/>
<button
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
class="btn btn-success"
>
" class="form-control" />
<button style="border-top-left-radius: 0; border-bottom-left-radius: 0" class="btn btn-success">
+
</button>
</form>
@ -231,14 +181,8 @@
</div>
<!-- Mapping Key AltRight to Key Windows -->
<div class="mb-3">
<label for="mapkey" class="form-label"
>Map Right Alt key to Windows key</label
>
<select
id="mapkey"
class="form-select"
v-model="config.key_rightalt_to_key_win"
>
<label for="mapkey" class="form-label">Map Right Alt key to Windows key</label>
<select id="mapkey" class="form-select" v-model="config.key_rightalt_to_key_win">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
@ -275,21 +219,13 @@
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="'prep-cmd-admin-' + i"
v-model="c.elevated"
true-value="true"
false-value="false"
/>
<label :for="'prep-cmd-admin-' + i" class="form-check-label"
>Elevated</label
>
<input type="checkbox" class="form-check-input" :id="'prep-cmd-admin-' + i" v-model="c.elevated"
true-value="true" false-value="false" />
<label :for="'prep-cmd-admin-' + i" class="form-check-label">Elevated</label>
</div>
</td>
<td>
<button class="btn btn-danger" @click="$delete(global_prep_cmd, i)">
<button class="btn btn-danger" @click="global_prep_cmd.splice(i,1)">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="add_global_prep_cmd">
@ -309,42 +245,25 @@
<!--Private Key-->
<div class="mb-3">
<label for="pkey" class="form-label">Private Key</label>
<input
type="text"
class="form-control"
id="pkey"
placeholder="/dir/pkey.pem"
v-model="config.pkey"
/>
<div class="form-text">
The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key.
</div>
<input type="text" class="form-control" id="pkey" placeholder="/dir/pkey.pem" v-model="config.pkey" />
<div class="form-text">The private key used for the web UI and Moonlight client pairing. For best
compatibility, this should be an RSA-2048 private key.</div>
</div>
<!--Certificate-->
<div class="mb-3">
<label for="cert" class="form-label">Certificate</label>
<input
type="text"
class="form-control"
id="cert"
placeholder="/dir/cert.pem"
v-model="config.cert"
/>
<input type="text" class="form-control" id="cert" placeholder="/dir/cert.pem" v-model="config.cert" />
<div class="form-text">
The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.
The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have
an RSA-2048 public key.
</div>
</div>
<!--State File-->
<div class="mb-3">
<label for="file_state" class="form-label">State File</label>
<input
type="text"
class="form-control"
id="file_state"
placeholder="sunshine_state.json"
v-model="config.file_state"
/>
<input type="text" class="form-control" id="file_state" placeholder="sunshine_state.json"
v-model="config.file_state" />
<div class="form-text">
The file where current state of Sunshine is stored
</div>
@ -352,13 +271,7 @@
<!--Apps File-->
<div class="mb-3">
<label for="file_apps" class="form-label">Apps File</label>
<input
type="text"
class="form-control"
id="file_apps"
placeholder="apps.json"
v-model="config.file_apps"
/>
<input type="text" class="form-control" id="file_apps" placeholder="apps.json" v-model="config.file_apps" />
<div class="form-text">
The file where current apps of Sunshine are stored
</div>
@ -367,31 +280,21 @@
<div v-if="currentTab === 'input'" class="config-page">
<!--Home/Guide Button Emulation Timeout-->
<div class="mb-3">
<label for="back_button_timeout" class="form-label"
>Home/Guide Button Emulation Timeout</label
>
<input
type="text"
class="form-control"
id="back_button_timeout"
placeholder="-1"
v-model="config.back_button_timeout"
/>
<label for="back_button_timeout" class="form-label">Home/Guide Button Emulation Timeout</label>
<input type="text" class="form-control" id="back_button_timeout" placeholder="-1"
v-model="config.back_button_timeout" />
<div class="form-text">
If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated.<br />
If set to a value &lt; 0 (default), holding the Back/Select button will not emulate the Home/Guide button.<br />
If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press
is
emulated.<br />
If set to a value &lt; 0 (default), holding the Back/Select button will not emulate the Home/Guide
button.<br />
</div>
</div>
<!--Enable Mouse Input-->
<div class="mb-3">
<label for="mouse" class="form-label"
>Enable Mouse Input</label
>
<select
id="mouse"
class="form-select"
v-model="config.mouse"
>
<label for="mouse" class="form-label">Enable Mouse Input</label>
<select id="mouse" class="form-select" v-model="config.mouse">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
@ -401,14 +304,8 @@
</div>
<!--Enable Keyboard Input-->
<div class="mb-3">
<label for="keyboard" class="form-label"
>Enable Keyboard Input</label
>
<select
id="keyboard"
class="form-select"
v-model="config.keyboard"
>
<label for="keyboard" class="form-label">Enable Keyboard Input</label>
<select id="keyboard" class="form-select" v-model="config.keyboard">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
@ -418,14 +315,8 @@
</div>
<!--Enable Gamepad Input-->
<div class="mb-3">
<label for="gamepad" class="form-label"
>Enable Gamepad Input</label
>
<select
id="gamepad"
class="form-select"
v-model="config.controller"
>
<label for="gamepad" class="form-label">Enable Gamepad Input</label>
<select id="gamepad" class="form-select" v-model="config.controller">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
@ -435,16 +326,9 @@
</div>
<!-- Key Repeat Delay-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_delay" class="form-label"
>Key Repeat Delay</label
>
<input
type="text"
class="form-control"
id="key_repeat_delay"
placeholder="500"
v-model="config.key_repeat_delay"
/>
<label for="key_repeat_delay" class="form-label">Key Repeat Delay</label>
<input type="text" class="form-control" id="key_repeat_delay" placeholder="500"
v-model="config.key_repeat_delay" />
<div class="form-text">
Control how fast keys will repeat themselves<br />
The initial delay in milliseconds before repeating keys
@ -452,16 +336,9 @@
</div>
<!-- Key Repeat Frequency-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_frequency" class="form-label"
>Key Repeat Frequency</label
>
<input
type="text"
class="form-control"
id="key_repeat_frequency"
placeholder="24.9"
v-model="config.key_repeat_frequency"
/>
<label for="key_repeat_frequency" class="form-label">Key Repeat Frequency</label>
<input type="text" class="form-control" id="key_repeat_frequency" placeholder="24.9"
v-model="config.key_repeat_frequency" />
<div class="form-text">
How often keys repeat every second<br />
This configurable option supports decimals
@ -469,14 +346,8 @@
</div>
<!-- Always send scancodes -->
<div class="mb-3" v-if="platform === 'windows'">
<label for="always_send_scancodes" class="form-label"
>Always Send Scancodes</label
>
<select
id="always_send_scancodes"
class="form-select"
v-model="config.always_send_scancodes"
>
<label for="always_send_scancodes" class="form-label">Always Send Scancodes</label>
<select id="always_send_scancodes" class="form-select" v-model="config.always_send_scancodes">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled</option>
</select>
@ -494,29 +365,20 @@
<!--Audio Sink-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="audio_sink" class="form-label">Audio Sink</label>
<input
type="text"
class="form-control"
id="audio_sink"
placeholder="Speakers (High Definition Audio Device)"
v-model="config.audio_sink"
/>
<input type="text" class="form-control" id="audio_sink" placeholder="Speakers (High Definition Audio Device)"
v-model="config.audio_sink" />
<div class="form-text">
Manually specify a specific audio device to capture. If unset, the device is chosen automatically.<br />
<b>We strongly recommend leaving this field blank to use automatic device selection!</b><br />
If you have multiple audio devices with identical names, you can get the Device ID using the following command:<br />
If you have multiple audio devices with identical names, you can get the Device ID using the following
command:<br />
<pre>tools\audio-info.exe</pre>
</div>
</div>
<div class="mb-3" v-if="platform === 'linux'">
<label for="audio_sink" class="form-label">Audio Sink</label>
<input
type="text"
class="form-control"
id="audio_sink"
placeholder="alsa_output.pci-0000_09_00.3.analog-stereo"
v-model="config.audio_sink"
/>
<input type="text" class="form-control" id="audio_sink"
placeholder="alsa_output.pci-0000_09_00.3.analog-stereo" v-model="config.audio_sink" />
<div class="form-text">
The name of the audio sink used for Audio Loopback<br />
If you do not specify this variable, pulseaudio will select the default monitor device.<br />
@ -529,23 +391,14 @@
</div>
<div class="mb-3" v-if="platform === 'macos'">
<label for="audio_sink" class="form-label">Audio Sink</label>
<input
type="text"
class="form-control"
id="audio_sink"
placeholder="BlackHole 2ch"
v-model="config.audio_sink"
/>
<input type="text" class="form-control" id="audio_sink" placeholder="BlackHole 2ch"
v-model="config.audio_sink" />
<div class="form-text">
The name of the audio sink used for Audio Loopback<br />
Sunshine can only access microphones on macOS due to system limitations.<br />
To stream system audio using <a
href="https://github.com/mattingalls/Soundflower"
target="_blank">
To stream system audio using <a href="https://github.com/mattingalls/Soundflower" target="_blank">
Soundflower
</a> or <a
href="https://github.com/ExistentialAudio/BlackHole"
target="_blank">
</a> or <a href="https://github.com/ExistentialAudio/BlackHole" target="_blank">
BlackHole
</a>.
</div>
@ -553,13 +406,8 @@
<!--Virtual Sink-->
<div class="mb-3" v-if="platform === 'windows'">
<label for="virtual_sink" class="form-label">Virtual Sink</label>
<input
type="text"
class="form-control"
id="virtual_sink"
placeholder="Steam Streaming Speakers"
v-model="config.virtual_sink"
/>
<input type="text" class="form-control" id="virtual_sink" placeholder="Steam Streaming Speakers"
v-model="config.virtual_sink" />
<div class="form-text">
Manually specify a virtual audio device to use. If unset, the device is chosen automatically.<br />
<b>We strongly recommend leaving this field blank to use automatic device selection!</b><br />
@ -580,13 +428,8 @@
<!--Adapter Name -->
<div class="mb-3" v-if="platform === 'windows'">
<label for="adapter_name" class="form-label">Adapter Name</label>
<input
type="text"
class="form-control"
id="adapter_name"
placeholder="Radeon RX 580 Series"
v-model="config.adapter_name"
/>
<input type="text" class="form-control" id="adapter_name" placeholder="Radeon RX 580 Series"
v-model="config.adapter_name" />
<div class="form-text" v-if="platform === 'windows'">
Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically.<br />
<b>We strongly recommend leaving this field blank to use automatic GPU selection!</b><br />
@ -598,13 +441,8 @@
<!--Output Name -->
<div class="mb-3" v-if="platform === 'windows'">
<label for="output_name" class="form-label">Output Name</label>
<input
type="text"
class="form-control"
id="output_name"
placeholder="\\.\DISPLAY1"
v-model="config.output_name"
/>
<input type="text" class="form-control" id="output_name" placeholder="\\.\DISPLAY1"
v-model="config.output_name" />
<div class="form-text">
Manually specify a display to use for capture. If unset, the primary display is captured.<br />
Note: If you specified a GPU above, this display must be connected to that GPU.<br />
@ -614,13 +452,7 @@
</div>
<div class="mb-3" v-if="platform === 'linux'">
<label for="output_name" class="form-label">Monitor number</label>
<input
type="text"
class="form-control"
id="output_name"
placeholder="0"
v-model="config.output_name"
/>
<input type="text" class="form-control" id="output_name" placeholder="0" v-model="config.output_name" />
<div class="form-text">
During Sunshine startup, you should see the list of detected monitors, e.g.:<br />
<br />
@ -640,11 +472,7 @@
<!--Address family-->
<div class="mb-3">
<label for="address_family" class="form-label">Address Family</label>
<select
id="address_family"
class="form-select"
v-model="config.address_family"
>
<select id="address_family" class="form-select" v-model="config.address_family">
<option value="ipv4">IPv4 only</option>
<option value="both">IPv4+IPv6</option>
</select>
@ -653,15 +481,8 @@
<!--Port family-->
<div class="mb-3">
<label for="port" class="form-label">Port</label>
<input
type="number"
min="1029"
max="65514"
class="form-control"
id="port"
placeholder="47989"
v-model="config.port"
/>
<input type="number" min="1029" max="65514" class="form-control" id="port" placeholder="47989"
v-model="config.port" />
<div class="form-text">Set the family of ports used by Sunshine</div>
<!-- Add warning if any port is less than 1024 -->
<div class="alert alert-danger" v-if="(+effectivePort - 5) < 1024">
@ -725,20 +546,15 @@
</table>
<!-- add warning about exposing web ui to the internet -->
<div class="alert alert-warning" v-if="config.origin_web_ui_allowed === 'wan'">
<i class="fa-solid fa-xl fa-triangle-exclamation"></i> Exposing the Web UI to the internet is a security risk!
<i class="fa-solid fa-xl fa-triangle-exclamation"></i> Exposing the Web UI to the internet is a security
risk!
Proceed at your own risk!
</div>
</div>
<!-- Quantization Parameter -->
<div class="mb-3">
<label for="qp" class="form-label">Quantization Parameter</label>
<input
type="number"
class="form-control"
id="qp"
placeholder="28"
v-model="config.qp"
/>
<input type="number" class="form-control" id="qp" placeholder="28" v-model="config.qp" />
<div class="form-text">
Quantization Parameter<br />
Some devices may not support Constant Bit Rate.<br />
@ -746,170 +562,6 @@
Higher value means more compression, but less quality<br />
</div>
</div>
<!-- Min Threads -->
<div class="mb-3">
<label for="min_threads" class="form-label"
>Minimum Software Encoding Thread Count</label
>
<input
type="number"
min="1"
class="form-control"
id="min_threads"
placeholder="1"
v-model="config.min_threads"
/>
<div class="form-text">
Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually<br />
worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest<br />
value that can reliably encode at your desired streaming settings on your hardware.
</div>
</div>
<!--HEVC Support -->
<div class="mb-3">
<label for="hevc_mode" class="form-label">HEVC Support</label>
<select id="hevc_mode" class="form-select" v-model="config.hevc_mode">
<option value="0">
Sunshine will advertise support for HEVC based on encoder capabilities (recommended)
</option>
<option value="1">
Sunshine will not advertise support for HEVC
</option>
<option value="2">
Sunshine will advertise support for HEVC Main profile
</option>
<option value="3">
Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles
</option>
</select>
<div class="form-text">
Allows the client to request HEVC Main or HEVC Main10 video streams.<br />
HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
</div>
</div>
<!--AV1 Support -->
<div class="mb-3">
<label for="av1_mode" class="form-label">AV1 Support</label>
<select id="av1_mode" class="form-select" v-model="config.av1_mode">
<option value="0">
Sunshine will advertise support for AV1 based on encoder capabilities (recommended)
</option>
<option value="1">
Sunshine will not advertise support for AV1
</option>
<option value="2">
Sunshine will advertise support for AV1 Main 8-bit profile
</option>
<option value="3">
Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles
</option>
</select>
<div class="form-text">
Allows the client to request AV1 Main 8-bit or 10-bit video streams.<br />
AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.
</div>
</div>
<!--Capture-->
<div class="mb-3" v-if="platform === 'linux'">
<label for="capture" class="form-label">Force a Specific Capture Method</label>
<select id="capture" class="form-select" v-model="config.capture">
<option value="">Autodetect</option>
<option value="nvfbc">NvFBC</option>
<option value="wlr">wlroots</option>
<option value="kms">KMS</option>
<option value="x11">X11</option>
</select>
<div class="form-text">
Force a specific capture method, otherwise Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.
</div>
</div>
<!--Encoder-->
<div class="mb-3">
<label for="encoder" class="form-label">Force a Specific Encoder</label>
<select id="encoder" class="form-select" v-model="config.encoder">
<option value="">Autodetect</option>
<option value="nvenc" v-if="platform === 'windows' || platform === 'linux'">NVIDIA NVENC</option>
<option value="quicksync" v-if="platform === 'windows'">Intel QuickSync</option>
<option value="amdvce" v-if="platform === 'windows'">AMD AMF/VCE</option>
<option value="vaapi" v-if="platform === 'linux'">VA-API</option>
<option value="videotoolbox" v-if="platform === 'macos'">VideoToolbox</option>
<option value="software">Software</option>
</select>
<div class="form-text">
Force a specific encoder, otherwise Sunshine will use the first encoder that is available<br />
Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.
</div>
</div>
<!--FEC Percentage-->
<div class="mb-3">
<label for="fec_percentage" class="form-label">FEC Percentage</label>
<input
type="text"
class="form-control"
id="fec_percentage"
placeholder="20"
v-model="config.fec_percentage"
/>
<div class="form-text">
Percentage of error correcting packets per data packet in each video frame.<br />
Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.<br />
The default value of 20 is what GeForce Experience uses.
</div>
</div>
<!--Channels-->
<div class="mb-3">
<label for="channels" class="form-label">Channels</label>
<input
type="text"
class="form-control"
id="channels"
placeholder="1"
v-model="config.channels"
/>
<div class="form-text">
When multicasting, it could be useful to have different configurations for each connected Client. For example:
<ul>
<li>
Clients connected through WAN and LAN have different bitrate constraints.
</li>
<li>
Decoders may require different settings for color
</li>
</ul>
Unlike simply broadcasting to multiple Client, this will generate distinct video streams.<br />
Note, CPU usage increases for each distinct video stream generated
</div>
</div>
<!--Credentials File-->
<div class="mb-3">
<label for="credentials_file" class="form-label"
>Web Manager Credentials File</label
>
<input
type="text"
class="form-control"
id="credentials_file"
placeholder="sunshine_state.json"
v-model="config.credentials_file"
/>
<div class="form-text">
Store Username/Password separately from Sunshine's state file.
</div>
</div>
<!--External IP-->
<div class="mb-3">
<label for="external_ip" class="form-label">External IP</label>
<input
type="text"
class="form-control"
id="external_ip"
placeholder="123.456.789.12"
v-model="config.external_ip"
/>
<div class="form-text">
If no external IP address is given, Sunshine will automatically detect external IP
</div>
</div>
</div>
<!--Software Settings-->
<div v-if="currentTab === 'sw'" class="config-page">
@ -927,18 +579,22 @@
<option value="veryslow">veryslow</option>
</select>
<div class="form-text">
Optimize the trade-off between encoding speed (encoded frames per second) and compression efficiency (quality per bit in the bitstream). Defaults to superfast.
Optimize the trade-off between encoding speed (encoded frames per second) and compression efficiency
(quality
per bit in the bitstream). Defaults to superfast.
</div>
</div>
<div class="mb-3">
<label for="sw_tune" class="form-label">SW Tune</label>
<select id="sw_tune" class="form-select" v-model="config.sw_tune">
<option value="film">film -- use for high quality movie content; lowers deblocking</option>
<option value="animation">animation -- good for cartoons; uses higher deblocking and more reference frames</option>
<option value="animation">animation -- good for cartoons; uses higher deblocking and more reference frames
</option>
<option value="grain">grain -- preserves the grain structure in old, grainy film material</option>
<option value="stillimage">stillimage -- good for slideshow-like content</option>
<option value="fastdecode">fastdecode -- allows faster decoding by disabling certain filters</option>
<option value="zerolatency">zerolatency -- good for fast encoding and low-latency streaming (default)</option>
<option value="zerolatency">zerolatency -- good for fast encoding and low-latency streaming (default)
</option>
</select>
<div class="form-text">
Tuning options, which are applied after the preset. Defaults to zerolatency.
@ -961,7 +617,9 @@
</select>
<div class="form-text">Higher numbers improve compression (quality at given bitrate) at the cost of
<strong>increased encoding latency</strong>.<br>
Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by
Recommended to change only when limited by network or decoder, otherwise similar effect can be
accomplished
by
increasing bitrate.
</div>
</div>
@ -973,9 +631,12 @@
<option value="full_res">Full resolution (slower)</option>
</select>
<div class="form-text">Adds preliminary encoding pass.<br>
This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to
This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly
adhere
to
bitrate limits.<br>
Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.
Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet
loss.
</div>
</div>
<div class="accordion">
@ -990,7 +651,9 @@
aria-labelledby="panelsStayOpen-headingOne">
<div class="accordion-body">
<div class="mb-3" v-if="platform === 'windows'">
<label for="nvenc_realtime_hags" class="form-label">Use realtime priority in hardware accelerated gpu scheduling</label>
<label for="nvenc_realtime_hags" class="form-label">Use realtime priority in hardware accelerated
gpu
scheduling</label>
<select id="nvenc_realtime_hags" class="form-select" v-model="config.nvenc_realtime_hags">
<option value="disabled">Disabled</option>
<option value="enabled">Enabled (default)</option>
@ -998,7 +661,8 @@
<div class="form-text">Currently NVIDIA drivers may freeze in encoder when
<a href="https://devblogs.microsoft.com/directx/hardware-accelerated-gpu-scheduling/">HAGS</a>
is enabled, realtime priority is used and VRAM utilization is close to maximum.<br>
Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture
Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced
capture
performance when the GPU is heavily loaded.
</div>
</div>
@ -1046,10 +710,7 @@
<!--Presets-->
<div class="mb-3">
<label for="amd_quality" class="form-label">AMF Quality</label>
<select
id="amd_quality"
class="form-select"
v-model="config.amd_quality">
<select id="amd_quality" class="form-select" v-model="config.amd_quality">
<option value="speed">speed -- prefer speed</option>
<option value="balanced">balanced -- balanced (default)</option>
<option value="quality">quality -- prefer quality</option>
@ -1098,12 +759,7 @@
</div>
<!--VA-API Encoder Settings-->
<div v-if="currentTab === 'va-api'" class="config-page">
<input
class="form-control"
id="adapter_name"
placeholder="/dev/dri/renderD128"
v-model="config.adapter_name"
/>
<input class="form-control" id="adapter_name" placeholder="/dev/dri/renderD128" v-model="config.adapter_name" />
</div>
<!--VideoToolbox Encoder Settings-->
<div v-if="currentTab === 'vt'" class="config-page">
@ -1135,19 +791,21 @@
</div>
</div>
<div class="alert alert-success my-4" v-if="saved && !restarted">
<i class="fa-solid fa-xl fa-circle-check"></i> Click 'Apply' to restart Sunshine and apply changes.
This will terminate any running sessions.
<b>Success!</b> Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.
</div>
<div class="alert alert-primary my-4" v-if="restarted">
<i class="fa-solid fa-xl fa-spinner fa-spin"></i> Sunshine is restarting to apply changes.
<div class="alert alert-success my-4" v-if="restarted">
<b>Success!</b> Sunshine is restarting to apply changes.
</div>
<div class="mb-3 buttons">
<button class="btn btn-primary" @click="save">Save</button>
<button class="btn btn-success" @click="apply" v-if="saved && !restarted">Apply</button>
</div>
</div>
</body>
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
<script>
// create dictionary for defaultConfig
const defaultConfig = {
"address_family": "ipv4",
@ -1189,8 +847,10 @@
"global_prep_cmd": "[]",
}
new Vue({
el: "#app",
const app = createApp({
components: {
Navbar
},
data() {
return {
platform: "",
@ -1401,22 +1061,6 @@
},
}
});
app.mount("#app");
</script>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
.ms-item {
background-color: #ccc;
font-size: 12px;
font-weight: bold;
}
</style>

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sunshine</title>
<link href="/node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="/node_modules/vue/dist/vue.min.js"></script>
</head>
<body></body>
</html>

View File

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sunshine</title>
<link rel="icon" type="image/x-icon" href="/images/sunshine.ico">
<link href="/node_modules/@fortawesome/fontawesome-free/css/all.min.css" rel="stylesheet">
<link href="/node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="/node_modules/vue/dist/vue.min.js"></script>
</head>
<body>
<nav
class="navbar navbar-expand-lg navbar-light"
style="background-color: #ffc400"
>
<div class="container-fluid">
<a class="navbar-brand" href="/" title="Sunshine">
<img src="/images/logo-sunshine-45.png" height="45" alt="Sunshine">
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-fw fa-home"></i> Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/pin"><i class="fas fa-fw fa-unlock"></i> PIN</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/apps"><i class="fas fa-fw fa-stream"></i> Applications</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config"><i class="fas fa-fw fa-cog"></i> Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/password"><i class="fas fa-fw fa-user-shield"></i> Change Password</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/troubleshooting"><i class="fas fa-fw fa-info"></i> Troubleshooting</a>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>
<script>
let el = document.querySelector("a[href='"+document.location.pathname+"']");
if(el)el.classList.add("active")
</script>
<style>
.nav-link.active {
font-weight: 500;
}
.form-control::placeholder {
opacity: 0.5;
}
</style>
<!-- Discord WidgetBot Crate-->
<script src="https://cdn.jsdelivr.net/npm/@widgetbot/crate@3" async defer>
new Crate({
server: '804382334370578482',
channel: '804383092822900797',
defer: false,
})
</script>

View File

@ -1,3 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
</head>
<body id="app">
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">Hello, Sunshine!</h1>
<p>Sunshine is a self-hosted game stream host for Moonlight.</p>
@ -51,43 +60,22 @@
</div>
</div>
<!--Resources-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Resources</h2>
<br />
<p>
Resources for Sunshine!
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-success m-1" href="https://app.lizardbyte.dev" target="_blank">LizardByte Website</a>
<a class="btn btn-primary m-1" href="https://app.lizardbyte.dev/discord" target="_blank">
<i class="fab fa-fw fa-discord"></i> Discord</a>
<a class="btn btn-secondary m-1" href="https://github.com/LizardByte/Sunshine/discussions" target="_blank">
<i class="fab fa-fw fa-github"></i> Github Discussions</a>
</div>
</div>
</div>
<!--Legal-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Legal</h2>
<br />
<p>
By continuing to use this software you agree to the terms and conditions in the following documents.
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/LICENSE" target="_blank">
<i class="fas fa-fw fa-file-alt"></i> License</a>
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/NOTICE" target="_blank">
<i class="fas fa-fw fa-exclamation"></i> Third Party Notice</a>
</div>
</div>
<div class="my-4">
<Resource-Card></Resource-Card>
</div>
</div>
</body>
<script>
new Vue({
el: "#content",
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
import ResourceCard from './ResourceCard.vue'
console.log("Hello, Sunshine!")
let app = createApp({
components: {
Navbar,
ResourceCard
},
data() {
return {
version: null,
@ -165,4 +153,5 @@
}
}
});
app.mount('#app');
</script>

View File

@ -1,4 +1,24 @@
<div id="app" class="container">
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<h1 class="my-4">Password Change</h1>
<form @submit.prevent="save">
<div class="card d-flex p-4 flex-row">
@ -6,63 +26,34 @@
<h4>Current Credentials</h4>
<div class="mb-3">
<label for="currentUsername" class="form-label">Username</label>
<input
required
type="text"
class="form-control"
id="currentUsername"
v-model="passwordData.currentUsername"
/>
<input required type="text" class="form-control" id="currentUsername"
v-model="passwordData.currentUsername" />
<div class="form-text">&nbsp;</div>
</div>
<div class="mb-3">
<label for="currentPassword" class="form-label">Password</label>
<input
autocomplete="current-password"
type="password"
class="form-control"
id="currentPassword"
v-model="passwordData.currentPassword"
/>
<input autocomplete="current-password" type="password" class="form-control" id="currentPassword"
v-model="passwordData.currentPassword" />
</div>
</div>
<div class="col-md-6 px-4">
<h4>New Credentials</h4>
<div class="mb-3">
<label for="newUsername" class="form-label">New Username</label>
<input
type="text"
class="form-control"
id="newUsername"
v-model="passwordData.newUsername"
/>
<input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername" />
<div class="form-text">
If not specified, the username will not change
</div>
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">Password</label>
<input
autocomplete="new-password"
required
type="password"
class="form-control"
id="newPassword"
v-model="passwordData.newPassword"
/>
<input autocomplete="new-password" required type="password" class="form-control" id="newPassword"
v-model="passwordData.newPassword" />
</div>
<div class="mb-3">
<label for="confirmNewPassword" class="form-label"
>Confirm Password</label
>
<input
autocomplete="new-password"
required
type="password"
class="form-control"
id="confirmNewPassword"
v-model="passwordData.confirmNewPassword"
/>
<label for="confirmNewPassword" class="form-label">Confirm Password</label>
<input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword"
v-model="passwordData.confirmNewPassword" />
</div>
</div>
</div>
@ -76,10 +67,15 @@
</div>
</form>
</div>
</body>
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
<script>
new Vue({
el: "#app",
const app = createApp({
components: {
Navbar
},
data() {
return {
error: null,
@ -118,16 +114,6 @@
},
},
});
app.mount("#app");
</script>
<style>
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
.buttons {
padding: 1em 0;
}
</style>

View File

@ -1,13 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
</head>
<body id="app">
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">PIN Pairing</h1>
<form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4">
<input
type="number"
placeholder="PIN"
id="pin-input"
class="form-control my-4"
/>
<input type="text" pattern="\d*" placeholder="PIN" id="pin-input" class="form-control my-4" />
<button class="btn btn-primary">Send</button>
</div>
<div class="alert alert-warning">
@ -18,8 +22,18 @@
<div id="status"></div>
</form>
</div>
</body>
<script type="module">
import Navbar from './Navbar.vue'
import {createApp} from 'vue'
let app = createApp({
components: {
Navbar
}
});
app.mount("#app");
<script>
document.querySelector("#form").addEventListener("submit", (e) => {
e.preventDefault();
let pin = document.querySelector("#pin-input").value;

View File

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 650 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,9 @@
<!-- TEMPLATE_HEADER - Used by Every UI Page -->
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sunshine</title>
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
<link href="@fortawesome/fontawesome-free/css/all.min.css" rel="stylesheet">
<link href="bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<script type="module" src="bootstrap/dist/js/bootstrap.bundle.min.js"></script>

View File

@ -1,4 +1,44 @@
<div id="app" class="container">
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
<style>
.troubleshooting-logs {
white-space: pre;
font-family: monospace;
overflow: auto;
max-height: 500px;
min-height: 500px;
font-size: 16px;
position: relative;
}
.copy-icon {
position: absolute;
top: 8px;
right: 8px;
padding: 8px;
cursor: pointer;
color: rgba(0, 0, 0, 1);
appearance: none;
border: none;
background: none;
}
.copy-icon:hover {
color: rgba(0, 0, 0, 0.75);
}
.copy-icon:active {
color: rgba(0, 0, 0, 1);
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<h1 class="my-4">Troubleshooting</h1>
<!--Force Close App-->
<div class="card p-2 my-4">
@ -78,9 +118,14 @@
</div>
</div>
<script>
new Vue({
el: "#app",
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
const app = createApp({
components: {
Navbar
},
data() {
return {
closeAppPressed: false,
@ -156,35 +201,9 @@
},
},
});
app.mount("#app");
</script>
<style>
.troubleshooting-logs {
white-space: pre;
font-family: monospace;
overflow: auto;
max-height: 500px;
min-height: 500px;
font-size: 16px;
position: relative;
}
.copy-icon {
position: absolute;
top: 8px;
right: 8px;
padding: 8px;
cursor: pointer;
color: rgba(0,0,0,1);
appearance: none;
border: none;
background: none;
}
.copy-icon:hover {
color: rgba(0,0,0,0.75);
}
.copy-icon:active {
color: rgba(0,0,0,1);
}
</style>
</body>

View File

@ -1,10 +1,23 @@
<main role="main" id="app" style="max-width: 600px; margin: 0 auto">
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
</head>
<body id="app">
<main role="main" style="max-width: 1200px; margin: 1em auto">
<div class="d-flex gap-4">
<div class="card p-2">
<header>
<h1 class="mb-0">Welcome to Sunshine!</h1>
<p class="mb-0 align-self-start">
<h1 class="mb-0">
<img src="/images/logo-sunshine-45.png" height="45" alt="">
Welcome to Sunshine!
</h1>
</header>
<p class="my-2 align-self-start">
Before Getting Started, we need you to make a new username and password for accessing the Web UI.
</p>
</header>
<div class="alert alert-warning">
The credentials below are needed to access Sunshine's Web UI.<br />
Keep them safe, since <b>you will never see them again!</b>
@ -12,43 +25,20 @@
<form @submit.prevent="save">
<div class="mb-2">
<label for="usernameInput" class="form-label">Username:</label>
<input
type="text"
class="form-control"
id="usernameInput"
autocomplete="username"
v-model="passwordData.newUsername"
/>
<input type="text" class="form-control" id="usernameInput" autocomplete="username"
v-model="passwordData.newUsername" />
</div>
<div class="mb-2">
<label for="passwordInput" class="form-label">Password:</label>
<input
type="password"
class="form-control"
id="passwordInput"
autocomplete="new-password"
v-model="passwordData.newPassword"
required
/>
<input type="password" class="form-control" id="passwordInput" autocomplete="new-password"
v-model="passwordData.newPassword" required />
</div>
<div class="mb-2">
<label for="confirmPasswordInput" class="form-label"
>Password (confirm):</label
>
<input
type="password"
class="form-control"
id="confirmPasswordInput"
autocomplete="new-password"
v-model="passwordData.confirmNewPassword"
required
/>
<label for="confirmPasswordInput" class="form-label">Password (confirm):</label>
<input type="password" class="form-control" id="confirmPasswordInput" autocomplete="new-password"
v-model="passwordData.confirmNewPassword" required />
</div>
<button
type="submit"
class="btn btn-primary w-100 mb-2"
v-bind:disabled="loading"
>
<button type="submit" class="btn btn-primary w-100 mb-2" v-bind:disabled="loading">
Login
</button>
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
@ -57,11 +47,21 @@
the new credentials
</div>
</form>
</div>
<div>
<Resource-Card></Resource-Card>
</div>
</div>
</main>
</body>
<script>
new Vue({
el: "#app",
<script type="module">
import { createApp } from "vue"
import ResourceCard from './ResourceCard.vue'
let app = createApp({
components: {
ResourceCard
},
data() {
return {
error: null,
@ -101,4 +101,5 @@
},
},
});
app.mount("#app");
</script>

53
vite.config.js Normal file
View File

@ -0,0 +1,53 @@
import { fileURLToPath, URL } from 'node:url'
import fs from 'fs';
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { ViteEjsPlugin } from "vite-plugin-ejs";
import vue from '@vitejs/plugin-vue'
import process from 'process'
/**
* Before actually building the pages with Vite, we do an intermediate build step using ejs
* Importing this separately and joining them using ejs
* allows us to split some repeating HTML that cannot be added
* by Vue itself (e.g. style/script loading, common meta head tags, Widgetbot)
* The vite-plugin-ejs handles this automatically
*/
let assetsSrcPath = 'src_assets/common/assets/web';
let assetsDstPath = 'build/assets/web';
if (process.env.SUNSHINE_SOURCE_ASSETS_DIR) {
console.log("Using srcdir from Cmake: " + resolve(process.env.SUNSHINE_SOURCE_ASSETS_DIR,"common/assets/web"));
assetsSrcPath = resolve(process.env.SUNSHINE_SOURCE_ASSETS_DIR,"common/assets/web")
}
if (process.env.SUNSHINE_ASSETS_DIR) {
console.log("Using destdir from Cmake: " + resolve(process.env.SUNSHINE_ASSETS_DIR,"assets/web"));
assetsDstPath = resolve(process.env.SUNSHINE_ASSETS_DIR,"assets/web")
}
let header = fs.readFileSync(resolve(assetsSrcPath, "template_header.html"))
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js'
}
},
plugins: [vue(), ViteEjsPlugin({ header })],
root: resolve(assetsSrcPath),
build: {
outDir: resolve(assetsDstPath),
rollupOptions: {
input: {
apps: resolve(assetsSrcPath, 'apps.html'),
config: resolve(assetsSrcPath, 'config.html'),
index: resolve(assetsSrcPath, 'index.html'),
password: resolve(assetsSrcPath, 'password.html'),
pin: resolve(assetsSrcPath, 'pin.html'),
troubleshooting: resolve(assetsSrcPath, 'troubleshooting.html'),
welcome: resolve(assetsSrcPath, 'welcome.html'),
},
},
},
})