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
57 changed files with 1868 additions and 2177 deletions

View File

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

View File

@@ -84,3 +84,4 @@ elseif(UNIX)
include(${CMAKE_MODULE_PATH}/dependencies/linux.cmake) include(${CMAKE_MODULE_PATH}/dependencies/linux.cmake)
endif() endif()
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_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_STRIP_FILES YES) set(CPACK_STRIP_FILES YES)
# install npm modules #install common assets
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/node_modules" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}/web") 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 # platform specific packaging
if(WIN32) if(WIN32)

View File

@@ -70,11 +70,11 @@ if(${SUNSHINE_TRAY} STREQUAL 1)
install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg"
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status"
RENAME "sunshine-tray.svg") 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") 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") 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") DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/status")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ 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(MAC_PREFIX "${CMAKE_PROJECT_NAME}.app/Contents")
set(INSTALL_RUNTIME_DIR "${MAC_PREFIX}/MacOS") 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/" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}") DESTINATION "${SUNSHINE_ASSETS_DIR}")

View File

@@ -13,6 +13,3 @@ if(NOT CMAKE_INSTALL_PREFIX)
endif() endif()
install(TARGETS sunshine RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") 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) COMPONENT gamepad)
# Sunshine assets # Sunshine assets
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}"
COMPONENT assets)
install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/" install(DIRECTORY "${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/"
DESTINATION "${SUNSHINE_ASSETS_DIR}" DESTINATION "${SUNSHINE_ASSETS_DIR}"
COMPONENT assets) COMPONENT assets)

View File

@@ -33,3 +33,9 @@ foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)
endforeach() 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 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/ WORKDIR /build/sunshine/
COPY --link .. . COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory # setup build directory
WORKDIR /build/sunshine/build WORKDIR /build/sunshine/build

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ apt-get update -y
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
build-essential \ build-essential \
cmake=3.22.* \ cmake=3.22.* \
ca-certificates \
git \ git \
libayatana-appindicator3-dev \ libayatana-appindicator3-dev \
libavdevice-dev \ libavdevice-dev \
@@ -58,8 +59,6 @@ apt-get install -y --no-install-recommends \
libxfixes-dev \ libxfixes-dev \
libxrandr-dev \ libxrandr-dev \
libxtst-dev \ libxtst-dev \
nodejs \
npm \
wget wget
if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then if [[ "${TARGETPLATFORM}" == 'linux/amd64' ]]; then
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@@ -69,6 +68,17 @@ apt-get clean
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
_DEPS _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 # install cuda
WORKDIR /build/cuda WORKDIR /build/cuda
# versions: https://developer.nvidia.com/cuda-toolkit-archive # versions: https://developer.nvidia.com/cuda-toolkit-archive
@@ -95,16 +105,18 @@ _INSTALL_CUDA
WORKDIR /build/sunshine/ WORKDIR /build/sunshine/
COPY --link .. . COPY --link .. .
# setup npm dependencies
RUN npm install
# setup build directory # setup build directory
WORKDIR /build/sunshine/build WORKDIR /build/sunshine/build
# cmake and cpack # cmake and cpack
# hadolint ignore=SC1091
RUN <<_MAKE RUN <<_MAKE
#!/bin/bash #!/bin/bash
set -e set -e
#Set Node version
source "$HOME/.nvm/nvm.sh"
nvm use 20.9.0
#Actually build
cmake \ cmake \
-DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \ -DCMAKE_CUDA_COMPILER:PATH=/build/cuda/bin/nvcc \
-DCMAKE_BUILD_TYPE=Release \ -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 ./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm
rm ./cuda.run rm ./cuda.run
npm dependencies
----------------
Install npm dependencies.
.. code-block:: bash
npm install
Build Build
----- -----
.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing. .. 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 cd /usr/local/include
ln -s ../opt/openssl/include/openssl . ln -s ../opt/openssl/include/openssl .
npm dependencies
----------------
Install npm dependencies.
.. code-block:: bash
npm install
Build Build
----- -----
.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing. .. 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 \ 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-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-nodejs mingw-w64-x86_64-onevpl mingw-w64-x86_64-openssl \
mingw-w64-x86_64-toolchain 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
Build Build
----- -----

View File

@@ -69,7 +69,7 @@ source_suffix = ['.rst', '.md']
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# images # 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') html_logo = os.path.join(root_dir, 'sunshine.png')
# Add any paths that contain custom static files (such as style sheets) here, # 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 Read our contribution guide in our organization level
`docs <https://lizardbyte.readthedocs.io/en/latest/developers/contributing.html>`__. `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": { "dependencies": {
"@fortawesome/fontawesome-free": "6.4.2", "@fortawesome/fontawesome-free": "6.4.2",
"@popperjs/core": "2.11.8",
"@vitejs/plugin-vue": "4.3.4",
"bootstrap": "5.3.2", "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() { build() {
pushd "$pkgname"
npm install
popd
export BRANCH="@GITHUB_BRANCH@" export BRANCH="@GITHUB_BRANCH@"
export BUILD_VERSION="@GITHUB_BUILD_VERSION@" export BUILD_VERSION="@GITHUB_BUILD_VERSION@"
export COMMIT="@GITHUB_COMMIT@" export COMMIT="@GITHUB_COMMIT@"

View File

@@ -312,9 +312,6 @@ modules:
env: env:
npm_config_nodedir: /usr/lib/sdk/node18 npm_config_nodedir: /usr/lib/sdk/node18
NPM_CONFIG_LOGLEVEL: info NPM_CONFIG_LOGLEVEL: info
build-commands:
# Install npm dependencies
- cd ${FLATPAK_BUILDER_BUILDDIR} && npm install
config-opts: config-opts:
- -DCMAKE_BUILD_TYPE=Release - -DCMAKE_BUILD_TYPE=Release
- -DCMAKE_INSTALL_PREFIX=/app - -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 "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 "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." 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 "using icon sizes:"
echo ${icon_sizes[@]} echo ${icon_sizes[@]}
src_vectors=("../../src_assets/common/assets/web/images/sunshine-locked.svg" src_vectors=("../../src_assets/common/assets/web/public/images/sunshine-locked.svg"
"../../src_assets/common/assets/web/images/sunshine-pausing.svg" "../../src_assets/common/assets/web/public/images/sunshine-pausing.svg"
"../../src_assets/common/assets/web/images/sunshine-playing.svg" "../../src_assets/common/assets/web/public/images/sunshine-playing.svg"
"../../sunshine.svg") "../../sunshine.svg")
echo "using sources vectors:" echo "using sources vectors:"

View File

@@ -161,11 +161,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "index.html"); std::string content = read_file(WEB_DIR "index.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -174,11 +173,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "pin.html"); std::string content = read_file(WEB_DIR "pin.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -187,12 +185,11 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "apps.html"); std::string content = read_file(WEB_DIR "apps.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -201,11 +198,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "clients.html"); std::string content = read_file(WEB_DIR "clients.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -214,11 +210,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "config.html"); std::string content = read_file(WEB_DIR "config.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -227,11 +222,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "password.html"); std::string content = read_file(WEB_DIR "password.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -241,11 +235,10 @@ namespace confighttp {
send_redirect(response, request, "/"); send_redirect(response, request, "/");
return; return;
} }
std::string header = read_file(WEB_DIR "header-no-nav.html");
std::string content = read_file(WEB_DIR "welcome.html"); std::string content = read_file(WEB_DIR "welcome.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -254,11 +247,10 @@ namespace confighttp {
print_req(request); print_req(request);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "troubleshooting.html"); std::string content = read_file(WEB_DIR "troubleshooting.html");
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8"); headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(header + content, headers); response->write(content, headers);
} }
void void
@@ -295,14 +287,14 @@ namespace confighttp {
getNodeModules(resp_https_t response, req_https_t request) { getNodeModules(resp_https_t response, req_https_t request) {
print_req(request); print_req(request);
fs::path webDirPath(WEB_DIR); 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 // .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()); 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)) { 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"); response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request");
} }
else if (!fs::exists(filePath)) { else if (!fs::exists(filePath)) {
@@ -757,7 +749,7 @@ namespace confighttp {
server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage; server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; 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.reuse_address = true;
server.config.address = net::af_to_any_address_string(address_family); server.config.address = net::af_to_any_address_string(address_family);
server.config.port = port_https; 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"> <div class="my-4">
<h1>Applications</h1> <h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div> <div>Applications are refreshed only when Client is restarted</div>
@@ -15,10 +89,10 @@
<tr v-for="(app,i) in apps" :key="i"> <tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td> <td>{{app.name}}</td>
<td> <td>
<button class="btn btn-primary" @click="editApp(i)"> <button class="btn btn-primary mx-1" @click="editApp(i)">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</button> </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 <i class="fas fa-trash"></i> Delete
</button> </button>
</td> </td>
@@ -31,13 +105,7 @@
<!--name--> <!--name-->
<div class="mb-3"> <div class="mb-3">
<label for="appName" class="form-label">Application Name</label> <label for="appName" class="form-label">Application Name</label>
<input <input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name" />
type="text"
class="form-control"
id="appName"
aria-describedby="appNameHelp"
v-model="editForm.name"
/>
<div id="appNameHelp" class="form-text"> <div id="appNameHelp" class="form-text">
Application Name, as shown on Moonlight Application Name, as shown on Moonlight
</div> </div>
@@ -45,13 +113,8 @@
<!--output--> <!--output-->
<div class="mb-3"> <div class="mb-3">
<label for="appOutput" class="form-label">Output</label> <label for="appOutput" class="form-label">Output</label>
<input <input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
type="text" v-model="editForm.output" />
class="form-control monospace"
id="appOutput"
aria-describedby="appOutputHelp"
v-model="editForm.output"
/>
<div id="appOutputHelp" class="form-text"> <div id="appOutputHelp" class="form-text">
The file where the output of the command is stored, if it is not The file where the output of the command is stored, if it is not
specified, the output is ignored specified, the output is ignored
@@ -59,14 +122,8 @@
</div> </div>
<!--prep-cmd--> <!--prep-cmd-->
<div class="mb-3"> <div class="mb-3">
<label for="excludeGlobalPrep" class="form-label" <label for="excludeGlobalPrep" class="form-label">Global Prep Commands</label>
>Global Prep Commands</label <select id="excludeGlobalPrep" class="form-select" v-model="editForm['exclude-global-prep-cmd']">
>
<select
id="excludeGlobalPrep"
class="form-select"
v-model="editForm['exclude-global-prep-cmd']"
>
<option v-for="val in [false, true]" :value="val"> <option v-for="val in [false, true]" :value="val">
{{ !val ? 'Enabled' : 'Disabled' }} {{ !val ? 'Enabled' : 'Disabled' }}
</option> </option>
@@ -82,10 +139,7 @@
A list of commands to be run before/after this application.<br /> A list of commands to be run before/after this application.<br />
If any of the prep-commands fail, starting the application is aborted. If any of the prep-commands fail, starting the application is aborted.
</div> </div>
<div <div class="d-flex justify-content-start mb-3 mt-3" v-if="editForm['prep-cmd'].length === 0">
class="d-flex justify-content-start mb-3 mt-3"
v-if="editForm['prep-cmd'].length === 0"
>
<button class="btn btn-success" @click="addPrepCmd"> <button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus mr-1"></i> Add Commands <i class="fas fa-plus mr-1"></i> Add Commands
</button> </button>
@@ -104,39 +158,20 @@
<tbody> <tbody>
<tr v-for="(c, i) in editForm['prep-cmd']"> <tr v-for="(c, i) in editForm['prep-cmd']">
<td> <td>
<input <input type="text" class="form-control monospace" v-model="c.do" />
type="text"
class="form-control monospace"
v-model="c.do"
/>
</td> </td>
<td> <td>
<input <input type="text" class="form-control monospace" v-model="c.undo" />
type="text"
class="form-control monospace"
v-model="c.undo"
/>
</td> </td>
<td v-if="platform === 'windows'"> <td v-if="platform === 'windows'">
<div class="form-check"> <div class="form-check">
<input <input type="checkbox" class="form-check-input" :id="'prep-cmd-admin-' + i" v-model="c.elevated"
type="checkbox" true-value="true" false-value="false" />
class="form-check-input" <label :for="'prep-cmd-admin-' + i" class="form-check-label">Elevated</label>
: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> </div>
</td> </td>
<td> <td>
<button <button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">
class="btn btn-danger"
@click="$delete(editForm['prep-cmd'], i)"
>
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
<button class="btn btn-success" @click="addPrepCmd"> <button class="btn btn-success" @click="addPrepCmd">
@@ -147,32 +182,18 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!--detatched--> <!--detached-->
<div class="mb-3"> <div class="mb-3">
<label for="appName" class="form-label">Detached Commands</label> <label for="appName" class="form-label">Detached Commands</label>
<div <div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
v-for="(c,i) in editForm.detached" <input type="text" v-model="editForm.detached[i]" class="form-control monospace">
class="d-flex justify-content-between my-2" <button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">
>
<pre>{{c}}</pre>
<button
class="btn btn-danger mx-2"
@click="editForm.detached.splice(i,1)"
>
&times; &times;
</button> </button>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<input <button class="btn btn-success" @click="editForm.detached.push('');">
type="text" <i class="fas fa-plus mr-1"></i> Add Detached Command
class="form-control monospace"
v-model="detachedCmd"
/>
<button
class="btn btn-success mx-2"
@click="editForm.detached.push(detachedCmd);detachedCmd = '';"
>
+
</button> </button>
</div> </div>
<div class="form-text"> <div class="form-text">
@@ -182,13 +203,8 @@
<!--command--> <!--command-->
<div class="mb-3"> <div class="mb-3">
<label for="appCmd" class="form-label">Command</label> <label for="appCmd" class="form-label">Command</label>
<input <input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp"
type="text" v-model="editForm.cmd" />
class="form-control monospace"
id="appCmd"
aria-describedby="appCmdHelp"
v-model="editForm.cmd"
/>
<div id="appCmdHelp" class="form-text"> <div id="appCmdHelp" class="form-text">
The main application, if it is not specified, a process is started The main application, if it is not specified, a process is started
that sleeps indefinitely that sleeps indefinitely
@@ -197,13 +213,8 @@
<!--working dir--> <!--working dir-->
<div class="mb-3"> <div class="mb-3">
<label for="appWorkingDir" class="form-label">Working Directory</label> <label for="appWorkingDir" class="form-label">Working Directory</label>
<input <input type="text" class="form-control monospace" id="appWorkingDir" aria-describedby="appWorkingDirHelp"
type="text" v-model="editForm['working-dir']" />
class="form-control monospace"
id="appWorkingDir"
aria-describedby="appWorkingDirHelp"
v-model="editForm['working-dir']"
/>
<div id="appWorkingDirHelp" class="form-text"> <div id="appWorkingDirHelp" class="form-text">
The working directory that should be passed to the process. For The working directory that should be passed to the process. For
example, some applications use the working directory to search for example, some applications use the working directory to search for
@@ -213,17 +224,9 @@
</div> </div>
<!-- elevation --> <!-- elevation -->
<div class="mb-3 form-check" v-if="platform === 'windows'"> <div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="appElevation" class="form-check-label" <label for="appElevation" class="form-check-label">Run as administrator</label>
>Run as administrator</label <input type="checkbox" class="form-check-input" id="appElevation" v-model="editForm.elevated"
> true-value="true" false-value="false" />
<input
type="checkbox"
class="form-check-input"
id="appElevation"
v-model="editForm.elevated"
true-value="true"
false-value="false"
/>
<div class="form-text"> <div class="form-text">
This can be necessary for some applications that require administrator This can be necessary for some applications that require administrator
permissions to run properly. permissions to run properly.
@@ -231,80 +234,41 @@
</div> </div>
<!-- auto-detach --> <!-- auto-detach -->
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<label for="autoDetach" class="form-check-label" <label for="autoDetach" class="form-check-label">Continue streaming if the application exits quickly</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" />
<input
type="checkbox"
class="form-check-input"
id="autoDetach"
v-model="editForm['auto-detach']"
true-value="true"
false-value="false"
/>
<div class="form-text"> <div class="form-text">
This will attempt to automatically detect launcher-type apps that close This will attempt to automatically detect launcher-type apps that close
quickly after launching another program or instance of themselves. When quickly after launching another program or instance of themselves. When
a launcher-type app is detected, it is treated as a detached app. a launcher-type app is detected, it is treated as a detached app.
</div> </div>
</div> </div>
<!-- Image path -->
<div class="mb-3"> <div class="mb-3">
<label for="appImagePath" class="form-label">Image</label> <label for="appImagePath" class="form-label">Image</label>
<div class="input-group dropup"> <div class="input-group dropup">
<input <input type="text" class="form-control monospace" id="appImagePath" aria-describedby="appImagePathHelp"
type="text" v-model="editForm['image-path']" />
class="form-control monospace" <button class="btn btn-secondary dropdown-toggle" type="button" id="findCoverToggle"
id="appImagePath" aria-expanded="false" @click="showCoverFinder" ref="coverFinderDropdown">
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"
>
Find Cover Find Cover
</button> </button>
<div <div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden" aria-labelledby="findCoverToggle">
aria-labelledby="findCoverToggle" <div class="modal-header px-2">
>
<div class="modal-header">
<h4 class="modal-title">Covers Found</h4> <h4 class="modal-title">Covers Found</h4>
<button <button type="button" class="btn-close mr-2" aria-label="Close" @click="closeCoverFinder"></button>
type="button"
class="btn-close"
aria-label="Close"
@click="closeCoverFinder"
></button>
</div> </div>
<div <div class="modal-body cover-results px-3 pt-3" :class="{ busy: coverFinderBusy }">
class="modal-body cover-results px-3 pt-3"
:class="{ busy: coverFinderBusy }"
>
<div class="row"> <div class="row">
<div <div v-if="coverSearching" class="col-12 col-sm-6 col-lg-4 mb-3">
v-if="coverSearching"
class="col-12 col-sm-6 col-lg-4 mb-3"
>
<div class="cover-container"> <div class="cover-container">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
</div> </div>
<div <div v-for="(cover,i) in coverCandidates" :key="'i'" class="col-12 col-sm-6 col-lg-4 mb-3"
v-for="(cover,i) in coverCandidates" @click="useCover(cover)">
:key="'i'"
class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)"
>
<div class="cover-container result"> <div class="cover-container result">
<img class="rounded" :src="cover.url" /> <img class="rounded" :src="cover.url" />
</div> </div>
@@ -321,25 +285,74 @@
must be a PNG file. If not set, Sunshine will send default box image. must be a PNG file. If not set, Sunshine will send default box image.
</div> </div>
</div> </div>
<div class="env-hint"> <div class="env-hint alert alert-info">
<div class="form-text"><b>About Environment Variables: </b> All commands get these environment variables by default: </div> <div class="form-text">
<table> <h4>About Environment Variables</h4>
<tr><td><b>Var Name</b></td><td><b></b></td></tr> All commands get these environment variables by default:
<tr><td style="font-family: monospace">SUNSHINE_APP_ID</td><td>App ID</td></tr> </div>
<tr><td style="font-family: monospace">SUNSHINE_APP_NAME</td><td>App Name</td></tr> <table class="env-table">
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td><td>The Width requested by the client</td></tr> <tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td><td>The Height requested by the client</td></tr> <td><b>Var Name</b></td>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td><td>The FPS requested by the client</td></tr> <td><b></b></td>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td><td>(true/false) if HDR is enabled by the client</td></tr> </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>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td><td>(true/false) if the client has requested host audio</td></tr> <td style="font-family: monospace">SUNSHINE_APP_ID</td>
<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> <td>App ID</td>
<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> </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> </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-if="platform === 'windows'"><b>Example - QRes for Resolution
<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> Automation:</b>
<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> <pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre>
<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>
<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> </div>
<!--buttons--> <!--buttons-->
<div class="d-flex"> <div class="d-flex">
@@ -356,15 +369,15 @@
</button> </button>
</div> </div>
</div> </div>
<script> </body>
Vue.directive('dropdown-show', { <script type="module">
bind: function (el, binding) { import { createApp } from 'vue';
el.addEventListener('show.bs.dropdown', binding.value); import Navbar from './Navbar.vue'
} import {Dropdown} from 'bootstrap'
}); const app = createApp({
components: {
new Vue({ Navbar
el: "#app", },
data() { data() {
return { return {
apps: [], apps: [],
@@ -408,18 +421,18 @@
}, },
editApp(id) { editApp(id) {
this.editForm = JSON.parse(JSON.stringify(this.apps[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) if (this.editForm["prep-cmd"] === undefined)
this.$set(this.editForm, "prep-cmd", []); this.editForm["prep-cmd"] = [];
if (this.editForm["detached"] === undefined) if (this.editForm["detached"] === undefined)
this.$set(this.editForm, "detached", []); this.editForm["detached"] = [];
if (this.editForm["exclude-global-prep-cmd"] === undefined) 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') { if (this.editForm["elevated"] === undefined && this.platform === 'windows') {
this.$set(this.editForm, "elevated", false); this.editForm["elevated"] = [];
} }
if (this.editForm["auto-detach"] === undefined) { if (this.editForm["auto-detach"] === undefined) {
this.$set(this.editForm, "auto-detach", true); this.editForm["auto-detach"] = true;
} }
this.showEditForm = true; this.showEditForm = true;
}, },
@@ -448,7 +461,19 @@
showCoverFinder($event) { showCoverFinder($event) {
this.coverCandidates = []; this.coverCandidates = [];
this.coverSearching = true; 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) { function getSearchBucket(name) {
let bucket = name.substring(0, Math.min(name.length, 2)).toLowerCase().replaceAll(/[^a-z\d]/g, ''); let bucket = name.substring(0, Math.min(name.length, 2)).toLowerCase().replaceAll(/[^a-z\d]/g, '');
if (!bucket) { if (!bucket) {
@@ -503,7 +528,7 @@
if (!ref) { if (!ref) {
return; return;
} }
const dropdown = this.coverFinderDropdown = bootstrap.Dropdown.getInstance(ref); const dropdown = this.coverFinderDropdown = Dropdown.getInstance(ref);
if (!dropdown) { if (!dropdown) {
return; return;
} }
@@ -520,7 +545,7 @@
}).then(r => { }).then(r => {
if (!r.ok) throw new Error("Failed to download covers"); if (!r.ok) throw new Error("Failed to download covers");
return r.json(); return r.json();
}).then(body => this.$set(this.editForm, "image-path", body.path)) }).then(body => this.editForm["image-path"] = body.path)
.then(() => this.closeCoverFinder()) .then(() => this.closeCoverFinder())
.finally(() => this.coverFinderBusy = false); .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> </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> <h1 class="my-4">Configuration</h1>
<div class="form" v-if="config"> <div class="form" v-if="config">
<!--Header--> <!--Header-->
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="nav-item" v-for="tab in tabs" :key="tab.id"> <li class="nav-item" v-for="tab in tabs" :key="tab.id">
<a <a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
class="nav-link" @click="currentTab = tab.id">{{tab.name}}</a>
:class="{'active': tab.id === currentTab}"
href="#"
@click="currentTab = tab.id"
>{{tab.name}}</a
>
</li> </li>
</ul> </ul>
<!--General Tab--> <!--General Tab-->
@@ -18,13 +39,8 @@
<!--Sunshine Name--> <!--Sunshine Name-->
<div class="mb-3"> <div class="mb-3">
<label for="sunshine_name" class="form-label">Sunshine Name</label> <label for="sunshine_name" class="form-label">Sunshine Name</label>
<input <input type="text" class="form-control" id="sunshine_name" placeholder="Sunshine"
type="text" v-model="config.sunshine_name" />
class="form-control"
id="sunshine_name"
placeholder="Sunshine"
v-model="config.sunshine_name"
/>
<div class="form-text"> <div class="form-text">
The name displayed by Moonlight. If not specified, the PC's hostname is used The name displayed by Moonlight. If not specified, the PC's hostname is used
</div> </div>
@@ -32,11 +48,7 @@
<!--Log Level--> <!--Log Level-->
<div class="mb-3"> <div class="mb-3">
<label for="min_log_level" class="form-label">Log Level</label> <label for="min_log_level" class="form-label">Log Level</label>
<select <select id="min_log_level" class="form-select" v-model="config.min_log_level">
id="min_log_level"
class="form-select"
v-model="config.min_log_level"
>
<option value="0">Verbose</option> <option value="0">Verbose</option>
<option value="1">Debug</option> <option value="1">Debug</option>
<option value="2">Info</option> <option value="2">Info</option>
@@ -52,28 +64,15 @@
<!--Log Path--> <!--Log Path-->
<div class="mb-3"> <div class="mb-3">
<label for="log_path" class="form-label">Logfile Path</label> <label for="log_path" class="form-label">Logfile Path</label>
<input <input type="text" class="form-control" id="log_path" placeholder="sunshine.log" v-model="config.log_path" />
type="text"
class="form-control"
id="log_path"
placeholder="sunshine.log"
v-model="config.log_path"
/>
<div class="form-text"> <div class="form-text">
The file where the current logs of Sunshine are stored. The file where the current logs of Sunshine are stored.
</div> </div>
</div> </div>
<!--Origin Web UI Allowed--> <!--Origin Web UI Allowed-->
<div class="mb-3"> <div class="mb-3">
<label for="origin_web_ui_allowed" class="form-label" <label for="origin_web_ui_allowed" class="form-label">Origin Web UI Allowed</label>
>Origin Web UI Allowed</label <select id="origin_web_ui_allowed" class="form-select" v-model="config.origin_web_ui_allowed">
>
<select
id="origin_web_ui_allowed"
class="form-select"
v-model="config.origin_web_ui_allowed"
@change="forceUpdate"
>
<option value="pc">Only localhost may access Web UI</option> <option value="pc">Only localhost may access Web UI</option>
<option value="lan">Only those in LAN 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> <option value="wan">Anyone may access Web UI</option>
@@ -81,10 +80,6 @@
<div class="form-text"> <div class="form-text">
The origin of the remote endpoint address that is not denied access to Web UI The origin of the remote endpoint address that is not denied access to Web UI
</div> </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> </div>
<!--UPnP--> <!--UPnP-->
<div class="mb-3"> <div class="mb-3">
@@ -118,7 +113,8 @@
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
<label for="ds4_back_as_touchpad_click" class="form-label">Map Back/Select to Touchpad Click</label> <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="disabled">Disabled</option>
<option value="enabled">Enabled (default)</option> <option value="enabled">Enabled (default)</option>
</select> </select>
@@ -131,54 +127,27 @@
<!--Ping Timeout--> <!--Ping Timeout-->
<div class="mb-3"> <div class="mb-3">
<label for="ping_timeout" class="form-label">Ping Timeout</label> <label for="ping_timeout" class="form-label">Ping Timeout</label>
<input <input type="text" class="form-control" id="ping_timeout" placeholder="10000" v-model="config.ping_timeout" />
type="text"
class="form-control"
id="ping_timeout"
placeholder="10000"
v-model="config.ping_timeout"
/>
<div class="form-text"> <div class="form-text">
How long to wait in milliseconds for data from moonlight before shutting down the stream How long to wait in milliseconds for data from moonlight before shutting down the stream
</div> </div>
</div> </div>
<!--Advertised FPS and Resolutions--> <!--Advertised FPS and Resolutions-->
<div class="mb-3"> <div class="mb-3">
<label for="ping_timeout" class="form-label" <label for="ping_timeout" class="form-label">Advertised Resolutions and FPS</label>
>Advertised Resolutions and FPS</label
>
<div class="resolutions-container"> <div class="resolutions-container">
<label>Resolutions</label> <label>Resolutions</label>
<div class="resolutions d-flex flex-wrap"> <div class="resolutions d-flex flex-wrap">
<div <div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(r,i) in resolutions" :key="r">
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 class="px-2">{{r}}</span>
<span style="cursor: pointer" @click="resolutions.splice(i,1)" <span style="cursor: pointer" @click="resolutions.splice(i,1)">&times;</span>
>&times;</span
>
</div> </div>
<form <form @submit.prevent="resolutions.push(resIn);resIn = '';" class="d-flex align-items-center">
@submit.prevent="resolutions.push(resIn);resIn = '';" <input type="text" v-model="resIn" required pattern="[0-9]+x[0-9]+" style="
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-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
" " class="form-control" />
class="form-control" <button style="border-top-left-radius: 0; border-bottom-left-radius: 0" class="btn btn-success">
/>
<button
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
class="btn btn-success"
>
+ +
</button> </button>
</form> </form>
@@ -187,36 +156,17 @@
<div class="fps-container"> <div class="fps-container">
<label>FPS</label> <label>FPS</label>
<div class="fps d-flex flex-wrap"> <div class="fps d-flex flex-wrap">
<div <div class="p-2 ms-item m-2 d-flex justify-content-between" v-for="(f,i) in fps" :key="f">
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 class="px-2">{{f}}</span>
<span style="cursor: pointer" @click="fps.splice(i,1)" <span style="cursor: pointer" @click="fps.splice(i,1)">&times;</span>
>&times;</span
>
</div> </div>
<form <form @submit.prevent="fps.push(fpsIn);fpsIn = '';" class="d-flex align-items-center">
@submit.prevent="fps.push(fpsIn);fpsIn = '';" <input type="text" v-model="fpsIn" required pattern="[0-9]+" style="
class="d-flex align-items-center"
>
<input
type="text"
v-model="fpsIn"
required
pattern="[0-9]+"
style="
width: 6ch; width: 6ch;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
" " class="form-control" />
class="form-control" <button style="border-top-left-radius: 0; border-bottom-left-radius: 0" class="btn btn-success">
/>
<button
style="border-top-left-radius: 0; border-bottom-left-radius: 0"
class="btn btn-success"
>
+ +
</button> </button>
</form> </form>
@@ -231,14 +181,8 @@
</div> </div>
<!-- Mapping Key AltRight to Key Windows --> <!-- Mapping Key AltRight to Key Windows -->
<div class="mb-3"> <div class="mb-3">
<label for="mapkey" class="form-label" <label for="mapkey" class="form-label">Map Right Alt key to Windows key</label>
>Map Right Alt key to Windows key</label <select id="mapkey" class="form-select" v-model="config.key_rightalt_to_key_win">
>
<select
id="mapkey"
class="form-select"
v-model="config.key_rightalt_to_key_win"
>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
</select> </select>
@@ -275,21 +219,13 @@
</td> </td>
<td v-if="platform === 'windows'"> <td v-if="platform === 'windows'">
<div class="form-check"> <div class="form-check">
<input <input type="checkbox" class="form-check-input" :id="'prep-cmd-admin-' + i" v-model="c.elevated"
type="checkbox" true-value="true" false-value="false" />
class="form-check-input" <label :for="'prep-cmd-admin-' + i" class="form-check-label">Elevated</label>
: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> </div>
</td> </td>
<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> <i class="fas fa-trash"></i>
</button> </button>
<button class="btn btn-success" @click="add_global_prep_cmd"> <button class="btn btn-success" @click="add_global_prep_cmd">
@@ -309,42 +245,25 @@
<!--Private Key--> <!--Private Key-->
<div class="mb-3"> <div class="mb-3">
<label for="pkey" class="form-label">Private Key</label> <label for="pkey" class="form-label">Private Key</label>
<input <input type="text" class="form-control" id="pkey" placeholder="/dir/pkey.pem" v-model="config.pkey" />
type="text" <div class="form-text">The private key used for the web UI and Moonlight client pairing. For best
class="form-control" compatibility, this should be an RSA-2048 private key.</div>
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> </div>
<!--Certificate--> <!--Certificate-->
<div class="mb-3"> <div class="mb-3">
<label for="cert" class="form-label">Certificate</label> <label for="cert" class="form-label">Certificate</label>
<input <input type="text" class="form-control" id="cert" placeholder="/dir/cert.pem" v-model="config.cert" />
type="text"
class="form-control"
id="cert"
placeholder="/dir/cert.pem"
v-model="config.cert"
/>
<div class="form-text"> <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>
</div> </div>
<!--State File--> <!--State File-->
<div class="mb-3"> <div class="mb-3">
<label for="file_state" class="form-label">State File</label> <label for="file_state" class="form-label">State File</label>
<input <input type="text" class="form-control" id="file_state" placeholder="sunshine_state.json"
type="text" v-model="config.file_state" />
class="form-control"
id="file_state"
placeholder="sunshine_state.json"
v-model="config.file_state"
/>
<div class="form-text"> <div class="form-text">
The file where current state of Sunshine is stored The file where current state of Sunshine is stored
</div> </div>
@@ -352,13 +271,7 @@
<!--Apps File--> <!--Apps File-->
<div class="mb-3"> <div class="mb-3">
<label for="file_apps" class="form-label">Apps File</label> <label for="file_apps" class="form-label">Apps File</label>
<input <input type="text" class="form-control" id="file_apps" placeholder="apps.json" v-model="config.file_apps" />
type="text"
class="form-control"
id="file_apps"
placeholder="apps.json"
v-model="config.file_apps"
/>
<div class="form-text"> <div class="form-text">
The file where current apps of Sunshine are stored The file where current apps of Sunshine are stored
</div> </div>
@@ -367,31 +280,21 @@
<div v-if="currentTab === 'input'" class="config-page"> <div v-if="currentTab === 'input'" class="config-page">
<!--Home/Guide Button Emulation Timeout--> <!--Home/Guide Button Emulation Timeout-->
<div class="mb-3"> <div class="mb-3">
<label for="back_button_timeout" class="form-label" <label for="back_button_timeout" class="form-label">Home/Guide Button Emulation Timeout</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" />
<input
type="text"
class="form-control"
id="back_button_timeout"
placeholder="-1"
v-model="config.back_button_timeout"
/>
<div class="form-text"> <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 the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press
If set to a value &lt; 0 (default), holding the Back/Select button will not emulate the Home/Guide button.<br /> 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>
</div> </div>
<!--Enable Mouse Input--> <!--Enable Mouse Input-->
<div class="mb-3"> <div class="mb-3">
<label for="mouse" class="form-label" <label for="mouse" class="form-label">Enable Mouse Input</label>
>Enable Mouse Input</label <select id="mouse" class="form-select" v-model="config.mouse">
>
<select
id="mouse"
class="form-select"
v-model="config.mouse"
>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
</select> </select>
@@ -401,14 +304,8 @@
</div> </div>
<!--Enable Keyboard Input--> <!--Enable Keyboard Input-->
<div class="mb-3"> <div class="mb-3">
<label for="keyboard" class="form-label" <label for="keyboard" class="form-label">Enable Keyboard Input</label>
>Enable Keyboard Input</label <select id="keyboard" class="form-select" v-model="config.keyboard">
>
<select
id="keyboard"
class="form-select"
v-model="config.keyboard"
>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
</select> </select>
@@ -418,14 +315,8 @@
</div> </div>
<!--Enable Gamepad Input--> <!--Enable Gamepad Input-->
<div class="mb-3"> <div class="mb-3">
<label for="gamepad" class="form-label" <label for="gamepad" class="form-label">Enable Gamepad Input</label>
>Enable Gamepad Input</label <select id="gamepad" class="form-select" v-model="config.controller">
>
<select
id="gamepad"
class="form-select"
v-model="config.controller"
>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
</select> </select>
@@ -435,16 +326,9 @@
</div> </div>
<!-- Key Repeat Delay--> <!-- Key Repeat Delay-->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_delay" class="form-label" <label for="key_repeat_delay" class="form-label">Key Repeat Delay</label>
>Key Repeat Delay</label <input type="text" class="form-control" id="key_repeat_delay" placeholder="500"
> v-model="config.key_repeat_delay" />
<input
type="text"
class="form-control"
id="key_repeat_delay"
placeholder="500"
v-model="config.key_repeat_delay"
/>
<div class="form-text"> <div class="form-text">
Control how fast keys will repeat themselves<br /> Control how fast keys will repeat themselves<br />
The initial delay in milliseconds before repeating keys The initial delay in milliseconds before repeating keys
@@ -452,16 +336,9 @@
</div> </div>
<!-- Key Repeat Frequency--> <!-- Key Repeat Frequency-->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="key_repeat_frequency" class="form-label" <label for="key_repeat_frequency" class="form-label">Key Repeat Frequency</label>
>Key Repeat Frequency</label <input type="text" class="form-control" id="key_repeat_frequency" placeholder="24.9"
> v-model="config.key_repeat_frequency" />
<input
type="text"
class="form-control"
id="key_repeat_frequency"
placeholder="24.9"
v-model="config.key_repeat_frequency"
/>
<div class="form-text"> <div class="form-text">
How often keys repeat every second<br /> How often keys repeat every second<br />
This configurable option supports decimals This configurable option supports decimals
@@ -469,14 +346,8 @@
</div> </div>
<!-- Always send scancodes --> <!-- Always send scancodes -->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="always_send_scancodes" class="form-label" <label for="always_send_scancodes" class="form-label">Always Send Scancodes</label>
>Always Send Scancodes</label <select id="always_send_scancodes" class="form-select" v-model="config.always_send_scancodes">
>
<select
id="always_send_scancodes"
class="form-select"
v-model="config.always_send_scancodes"
>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled</option> <option value="enabled">Enabled</option>
</select> </select>
@@ -494,29 +365,20 @@
<!--Audio Sink--> <!--Audio Sink-->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="audio_sink" class="form-label">Audio Sink</label> <label for="audio_sink" class="form-label">Audio Sink</label>
<input <input type="text" class="form-control" id="audio_sink" placeholder="Speakers (High Definition Audio Device)"
type="text" v-model="config.audio_sink" />
class="form-control"
id="audio_sink"
placeholder="Speakers (High Definition Audio Device)"
v-model="config.audio_sink"
/>
<div class="form-text"> <div class="form-text">
Manually specify a specific audio device to capture. If unset, the device is chosen automatically.<br /> 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 /> <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> <pre>tools\audio-info.exe</pre>
</div> </div>
</div> </div>
<div class="mb-3" v-if="platform === 'linux'"> <div class="mb-3" v-if="platform === 'linux'">
<label for="audio_sink" class="form-label">Audio Sink</label> <label for="audio_sink" class="form-label">Audio Sink</label>
<input <input type="text" class="form-control" id="audio_sink"
type="text" placeholder="alsa_output.pci-0000_09_00.3.analog-stereo" v-model="config.audio_sink" />
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"> <div class="form-text">
The name of the audio sink used for Audio Loopback<br /> 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 /> If you do not specify this variable, pulseaudio will select the default monitor device.<br />
@@ -529,23 +391,14 @@
</div> </div>
<div class="mb-3" v-if="platform === 'macos'"> <div class="mb-3" v-if="platform === 'macos'">
<label for="audio_sink" class="form-label">Audio Sink</label> <label for="audio_sink" class="form-label">Audio Sink</label>
<input <input type="text" class="form-control" id="audio_sink" placeholder="BlackHole 2ch"
type="text" v-model="config.audio_sink" />
class="form-control"
id="audio_sink"
placeholder="BlackHole 2ch"
v-model="config.audio_sink"
/>
<div class="form-text"> <div class="form-text">
The name of the audio sink used for Audio Loopback<br /> The name of the audio sink used for Audio Loopback<br />
Sunshine can only access microphones on macOS due to system limitations.<br /> Sunshine can only access microphones on macOS due to system limitations.<br />
To stream system audio using <a To stream system audio using <a href="https://github.com/mattingalls/Soundflower" target="_blank">
href="https://github.com/mattingalls/Soundflower"
target="_blank">
Soundflower Soundflower
</a> or <a </a> or <a href="https://github.com/ExistentialAudio/BlackHole" target="_blank">
href="https://github.com/ExistentialAudio/BlackHole"
target="_blank">
BlackHole BlackHole
</a>. </a>.
</div> </div>
@@ -553,13 +406,8 @@
<!--Virtual Sink--> <!--Virtual Sink-->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="virtual_sink" class="form-label">Virtual Sink</label> <label for="virtual_sink" class="form-label">Virtual Sink</label>
<input <input type="text" class="form-control" id="virtual_sink" placeholder="Steam Streaming Speakers"
type="text" v-model="config.virtual_sink" />
class="form-control"
id="virtual_sink"
placeholder="Steam Streaming Speakers"
v-model="config.virtual_sink"
/>
<div class="form-text"> <div class="form-text">
Manually specify a virtual audio device to use. If unset, the device is chosen automatically.<br /> 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 /> <b>We strongly recommend leaving this field blank to use automatic device selection!</b><br />
@@ -580,13 +428,8 @@
<!--Adapter Name --> <!--Adapter Name -->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="adapter_name" class="form-label">Adapter Name</label> <label for="adapter_name" class="form-label">Adapter Name</label>
<input <input type="text" class="form-control" id="adapter_name" placeholder="Radeon RX 580 Series"
type="text" v-model="config.adapter_name" />
class="form-control"
id="adapter_name"
placeholder="Radeon RX 580 Series"
v-model="config.adapter_name"
/>
<div class="form-text" v-if="platform === 'windows'"> <div class="form-text" v-if="platform === 'windows'">
Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically.<br /> 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 /> <b>We strongly recommend leaving this field blank to use automatic GPU selection!</b><br />
@@ -598,13 +441,8 @@
<!--Output Name --> <!--Output Name -->
<div class="mb-3" v-if="platform === 'windows'"> <div class="mb-3" v-if="platform === 'windows'">
<label for="output_name" class="form-label">Output Name</label> <label for="output_name" class="form-label">Output Name</label>
<input <input type="text" class="form-control" id="output_name" placeholder="\\.\DISPLAY1"
type="text" v-model="config.output_name" />
class="form-control"
id="output_name"
placeholder="\\.\DISPLAY1"
v-model="config.output_name"
/>
<div class="form-text"> <div class="form-text">
Manually specify a display to use for capture. If unset, the primary display is captured.<br /> 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 /> Note: If you specified a GPU above, this display must be connected to that GPU.<br />
@@ -614,13 +452,7 @@
</div> </div>
<div class="mb-3" v-if="platform === 'linux'"> <div class="mb-3" v-if="platform === 'linux'">
<label for="output_name" class="form-label">Monitor number</label> <label for="output_name" class="form-label">Monitor number</label>
<input <input type="text" class="form-control" id="output_name" placeholder="0" v-model="config.output_name" />
type="text"
class="form-control"
id="output_name"
placeholder="0"
v-model="config.output_name"
/>
<div class="form-text"> <div class="form-text">
During Sunshine startup, you should see the list of detected monitors, e.g.:<br /> During Sunshine startup, you should see the list of detected monitors, e.g.:<br />
<br /> <br />
@@ -640,11 +472,7 @@
<!--Address family--> <!--Address family-->
<div class="mb-3"> <div class="mb-3">
<label for="address_family" class="form-label">Address Family</label> <label for="address_family" class="form-label">Address Family</label>
<select <select id="address_family" class="form-select" v-model="config.address_family">
id="address_family"
class="form-select"
v-model="config.address_family"
>
<option value="ipv4">IPv4 only</option> <option value="ipv4">IPv4 only</option>
<option value="both">IPv4+IPv6</option> <option value="both">IPv4+IPv6</option>
</select> </select>
@@ -653,15 +481,8 @@
<!--Port family--> <!--Port family-->
<div class="mb-3"> <div class="mb-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input <input type="number" min="1029" max="65514" class="form-control" id="port" placeholder="47989"
type="number" v-model="config.port" />
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> <div class="form-text">Set the family of ports used by Sunshine</div>
<!-- Add warning if any port is less than 1024 --> <!-- Add warning if any port is less than 1024 -->
<div class="alert alert-danger" v-if="(+effectivePort - 5) < 1024"> <div class="alert alert-danger" v-if="(+effectivePort - 5) < 1024">
@@ -725,20 +546,15 @@
</table> </table>
<!-- add warning about exposing web ui to the internet --> <!-- add warning about exposing web ui to the internet -->
<div class="alert alert-warning" v-if="config.origin_web_ui_allowed === 'wan'"> <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! Proceed at your own risk!
</div> </div>
</div> </div>
<!-- Quantization Parameter --> <!-- Quantization Parameter -->
<div class="mb-3"> <div class="mb-3">
<label for="qp" class="form-label">Quantization Parameter</label> <label for="qp" class="form-label">Quantization Parameter</label>
<input <input type="number" class="form-control" id="qp" placeholder="28" v-model="config.qp" />
type="number"
class="form-control"
id="qp"
placeholder="28"
v-model="config.qp"
/>
<div class="form-text"> <div class="form-text">
Quantization Parameter<br /> Quantization Parameter<br />
Some devices may not support Constant Bit Rate.<br /> Some devices may not support Constant Bit Rate.<br />
@@ -746,170 +562,6 @@
Higher value means more compression, but less quality<br /> Higher value means more compression, but less quality<br />
</div> </div>
</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> </div>
<!--Software Settings--> <!--Software Settings-->
<div v-if="currentTab === 'sw'" class="config-page"> <div v-if="currentTab === 'sw'" class="config-page">
@@ -927,18 +579,22 @@
<option value="veryslow">veryslow</option> <option value="veryslow">veryslow</option>
</select> </select>
<div class="form-text"> <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> </div>
<div class="mb-3"> <div class="mb-3">
<label for="sw_tune" class="form-label">SW Tune</label> <label for="sw_tune" class="form-label">SW Tune</label>
<select id="sw_tune" class="form-select" v-model="config.sw_tune"> <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="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="grain">grain -- preserves the grain structure in old, grainy film material</option>
<option value="stillimage">stillimage -- good for slideshow-like content</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="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> </select>
<div class="form-text"> <div class="form-text">
Tuning options, which are applied after the preset. Defaults to zerolatency. Tuning options, which are applied after the preset. Defaults to zerolatency.
@@ -961,7 +617,9 @@
</select> </select>
<div class="form-text">Higher numbers improve compression (quality at given bitrate) at the cost of <div class="form-text">Higher numbers improve compression (quality at given bitrate) at the cost of
<strong>increased encoding latency</strong>.<br> <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. increasing bitrate.
</div> </div>
</div> </div>
@@ -973,9 +631,12 @@
<option value="full_res">Full resolution (slower)</option> <option value="full_res">Full resolution (slower)</option>
</select> </select>
<div class="form-text">Adds preliminary encoding pass.<br> <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> 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> </div>
<div class="accordion"> <div class="accordion">
@@ -990,7 +651,9 @@
aria-labelledby="panelsStayOpen-headingOne"> aria-labelledby="panelsStayOpen-headingOne">
<div class="accordion-body"> <div class="accordion-body">
<div class="mb-3" v-if="platform === 'windows'"> <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"> <select id="nvenc_realtime_hags" class="form-select" v-model="config.nvenc_realtime_hags">
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="enabled">Enabled (default)</option> <option value="enabled">Enabled (default)</option>
@@ -998,7 +661,8 @@
<div class="form-text">Currently NVIDIA drivers may freeze in encoder when <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> <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> 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. performance when the GPU is heavily loaded.
</div> </div>
</div> </div>
@@ -1046,10 +710,7 @@
<!--Presets--> <!--Presets-->
<div class="mb-3"> <div class="mb-3">
<label for="amd_quality" class="form-label">AMF Quality</label> <label for="amd_quality" class="form-label">AMF Quality</label>
<select <select id="amd_quality" class="form-select" v-model="config.amd_quality">
id="amd_quality"
class="form-select"
v-model="config.amd_quality">
<option value="speed">speed -- prefer speed</option> <option value="speed">speed -- prefer speed</option>
<option value="balanced">balanced -- balanced (default)</option> <option value="balanced">balanced -- balanced (default)</option>
<option value="quality">quality -- prefer quality</option> <option value="quality">quality -- prefer quality</option>
@@ -1098,12 +759,7 @@
</div> </div>
<!--VA-API Encoder Settings--> <!--VA-API Encoder Settings-->
<div v-if="currentTab === 'va-api'" class="config-page"> <div v-if="currentTab === 'va-api'" class="config-page">
<input <input class="form-control" id="adapter_name" placeholder="/dev/dri/renderD128" v-model="config.adapter_name" />
class="form-control"
id="adapter_name"
placeholder="/dev/dri/renderD128"
v-model="config.adapter_name"
/>
</div> </div>
<!--VideoToolbox Encoder Settings--> <!--VideoToolbox Encoder Settings-->
<div v-if="currentTab === 'vt'" class="config-page"> <div v-if="currentTab === 'vt'" class="config-page">
@@ -1135,19 +791,21 @@
</div> </div>
</div> </div>
<div class="alert alert-success my-4" v-if="saved && !restarted"> <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. <b>Success!</b> Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.
This will terminate any running sessions.
</div> </div>
<div class="alert alert-primary my-4" v-if="restarted"> <div class="alert alert-success my-4" v-if="restarted">
<i class="fa-solid fa-xl fa-spinner fa-spin"></i> Sunshine is restarting to apply changes. <b>Success!</b> Sunshine is restarting to apply changes.
</div> </div>
<div class="mb-3 buttons"> <div class="mb-3 buttons">
<button class="btn btn-primary" @click="save">Save</button> <button class="btn btn-primary" @click="save">Save</button>
<button class="btn btn-success" @click="apply" v-if="saved && !restarted">Apply</button> <button class="btn btn-success" @click="apply" v-if="saved && !restarted">Apply</button>
</div> </div>
</div> </div>
</body>
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
<script>
// create dictionary for defaultConfig // create dictionary for defaultConfig
const defaultConfig = { const defaultConfig = {
"address_family": "ipv4", "address_family": "ipv4",
@@ -1189,8 +847,10 @@
"global_prep_cmd": "[]", "global_prep_cmd": "[]",
} }
new Vue({ const app = createApp({
el: "#app", components: {
Navbar
},
data() { data() {
return { return {
platform: "", platform: "",
@@ -1401,22 +1061,6 @@
}, },
} }
}); });
app.mount("#app");
</script> </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"> <div id="content" class="container">
<h1 class="my-4">Hello, Sunshine!</h1> <h1 class="my-4">Hello, Sunshine!</h1>
<p>Sunshine is a self-hosted game stream host for Moonlight.</p> <p>Sunshine is a self-hosted game stream host for Moonlight.</p>
@@ -51,43 +60,22 @@
</div> </div>
</div> </div>
<!--Resources--> <!--Resources-->
<div class="card p-2 my-4"> <div class="my-4">
<div class="card-body"> <Resource-Card></Resource-Card>
<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> </div>
</div> </div>
</body>
<script> <script type="module">
new Vue({ import { createApp } from 'vue'
el: "#content", import Navbar from './Navbar.vue'
import ResourceCard from './ResourceCard.vue'
console.log("Hello, Sunshine!")
let app = createApp({
components: {
Navbar,
ResourceCard
},
data() { data() {
return { return {
version: null, version: null,
@@ -165,4 +153,5 @@
} }
} }
}); });
app.mount('#app');
</script> </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> <h1 class="my-4">Password Change</h1>
<form @submit.prevent="save"> <form @submit.prevent="save">
<div class="card d-flex p-4 flex-row"> <div class="card d-flex p-4 flex-row">
@@ -6,63 +26,34 @@
<h4>Current Credentials</h4> <h4>Current Credentials</h4>
<div class="mb-3"> <div class="mb-3">
<label for="currentUsername" class="form-label">Username</label> <label for="currentUsername" class="form-label">Username</label>
<input <input required type="text" class="form-control" id="currentUsername"
required v-model="passwordData.currentUsername" />
type="text"
class="form-control"
id="currentUsername"
v-model="passwordData.currentUsername"
/>
<div class="form-text">&nbsp;</div> <div class="form-text">&nbsp;</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="currentPassword" class="form-label">Password</label> <label for="currentPassword" class="form-label">Password</label>
<input <input autocomplete="current-password" type="password" class="form-control" id="currentPassword"
autocomplete="current-password" v-model="passwordData.currentPassword" />
type="password"
class="form-control"
id="currentPassword"
v-model="passwordData.currentPassword"
/>
</div> </div>
</div> </div>
<div class="col-md-6 px-4"> <div class="col-md-6 px-4">
<h4>New Credentials</h4> <h4>New Credentials</h4>
<div class="mb-3"> <div class="mb-3">
<label for="newUsername" class="form-label">New Username</label> <label for="newUsername" class="form-label">New Username</label>
<input <input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername" />
type="text"
class="form-control"
id="newUsername"
v-model="passwordData.newUsername"
/>
<div class="form-text"> <div class="form-text">
If not specified, the username will not change If not specified, the username will not change
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="newPassword" class="form-label">Password</label> <label for="newPassword" class="form-label">Password</label>
<input <input autocomplete="new-password" required type="password" class="form-control" id="newPassword"
autocomplete="new-password" v-model="passwordData.newPassword" />
required
type="password"
class="form-control"
id="newPassword"
v-model="passwordData.newPassword"
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="confirmNewPassword" class="form-label" <label for="confirmNewPassword" class="form-label">Confirm Password</label>
>Confirm Password</label <input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword"
> v-model="passwordData.confirmNewPassword" />
<input
autocomplete="new-password"
required
type="password"
class="form-control"
id="confirmNewPassword"
v-model="passwordData.confirmNewPassword"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -76,10 +67,15 @@
</div> </div>
</form> </form>
</div> </div>
</body>
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
<script> const app = createApp({
new Vue({ components: {
el: "#app", Navbar
},
data() { data() {
return { return {
error: null, error: null,
@@ -118,16 +114,6 @@
}, },
}, },
}); });
app.mount("#app");
</script> </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"> <div id="content" class="container">
<h1 class="my-4">PIN Pairing</h1> <h1 class="my-4">PIN Pairing</h1>
<form action="" class="form d-flex flex-column align-items-center" id="form"> <form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4"> <div class="card flex-column d-flex p-4 mb-4">
<input <input type="text" pattern="\d*" placeholder="PIN" id="pin-input" class="form-control my-4" />
type="number"
placeholder="PIN"
id="pin-input"
class="form-control my-4"
/>
<button class="btn btn-primary">Send</button> <button class="btn btn-primary">Send</button>
</div> </div>
<div class="alert alert-warning"> <div class="alert alert-warning">
@@ -18,8 +22,18 @@
<div id="status"></div> <div id="status"></div>
</form> </form>
</div> </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) => { document.querySelector("#form").addEventListener("submit", (e) => {
e.preventDefault(); e.preventDefault();
let pin = document.querySelector("#pin-input").value; 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> <h1 class="my-4">Troubleshooting</h1>
<!--Force Close App--> <!--Force Close App-->
<div class="card p-2 my-4"> <div class="card p-2 my-4">
@@ -78,9 +118,14 @@
</div> </div>
</div> </div>
<script> <script type="module">
new Vue({ import { createApp } from 'vue'
el: "#app", import Navbar from './Navbar.vue'
const app = createApp({
components: {
Navbar
},
data() { data() {
return { return {
closeAppPressed: false, closeAppPressed: false,
@@ -156,35 +201,9 @@
}, },
}, },
}); });
app.mount("#app");
</script> </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 { </body>
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>

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> <header>
<h1 class="mb-0">Welcome to Sunshine!</h1> <h1 class="mb-0">
<p class="mb-0 align-self-start"> <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. Before Getting Started, we need you to make a new username and password for accessing the Web UI.
</p> </p>
</header>
<div class="alert alert-warning"> <div class="alert alert-warning">
The credentials below are needed to access Sunshine's Web UI.<br /> The credentials below are needed to access Sunshine's Web UI.<br />
Keep them safe, since <b>you will never see them again!</b> Keep them safe, since <b>you will never see them again!</b>
@@ -12,43 +25,20 @@
<form @submit.prevent="save"> <form @submit.prevent="save">
<div class="mb-2"> <div class="mb-2">
<label for="usernameInput" class="form-label">Username:</label> <label for="usernameInput" class="form-label">Username:</label>
<input <input type="text" class="form-control" id="usernameInput" autocomplete="username"
type="text" v-model="passwordData.newUsername" />
class="form-control"
id="usernameInput"
autocomplete="username"
v-model="passwordData.newUsername"
/>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="passwordInput" class="form-label">Password:</label> <label for="passwordInput" class="form-label">Password:</label>
<input <input type="password" class="form-control" id="passwordInput" autocomplete="new-password"
type="password" v-model="passwordData.newPassword" required />
class="form-control"
id="passwordInput"
autocomplete="new-password"
v-model="passwordData.newPassword"
required
/>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="confirmPasswordInput" class="form-label" <label for="confirmPasswordInput" class="form-label">Password (confirm):</label>
>Password (confirm):</label <input type="password" class="form-control" id="confirmPasswordInput" autocomplete="new-password"
> v-model="passwordData.confirmNewPassword" required />
<input
type="password"
class="form-control"
id="confirmPasswordInput"
autocomplete="new-password"
v-model="passwordData.confirmNewPassword"
required
/>
</div> </div>
<button <button type="submit" class="btn btn-primary w-100 mb-2" v-bind:disabled="loading">
type="submit"
class="btn btn-primary w-100 mb-2"
v-bind:disabled="loading"
>
Login Login
</button> </button>
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div> <div class="alert alert-danger" v-if="error"><b>Error: </b>{{error}}</div>
@@ -57,11 +47,21 @@
the new credentials the new credentials
</div> </div>
</form> </form>
</div>
<div>
<Resource-Card></Resource-Card>
</div>
</div>
</main> </main>
</body>
<script> <script type="module">
new Vue({ import { createApp } from "vue"
el: "#app", import ResourceCard from './ResourceCard.vue'
let app = createApp({
components: {
ResourceCard
},
data() { data() {
return { return {
error: null, error: null,
@@ -101,4 +101,5 @@
}, },
}, },
}); });
app.mount("#app");
</script> </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'),
},
},
},
})