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
commit 5bdbda90b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1868 additions and 2177 deletions

View file

@ -398,8 +398,6 @@ jobs:
mkdir -p build mkdir -p 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,370 +1,383 @@
<div id="app" class="container"> <!DOCTYPE html>
<div class="my-4"> <html lang="en">
<h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div> <head>
</div> <%- header %>
<div class="card p-4"> <style>
<table class="table"> .precmd-head {
<thead> width: 200px;
<tr> }
<th scope="col">Name</th>
<th scope="col">Actions</th> .monospace {
</tr> font-family: monospace;
</thead> }
<tbody>
<tr v-for="(app,i) in apps" :key="i"> .cover-finder {}
<td>{{app.name}}</td>
<td> .cover-finder .cover-results {
<button class="btn btn-primary" @click="editApp(i)"> max-height: 400px;
<i class="fas fa-edit"></i> Edit overflow-x: hidden;
overflow-y: auto;
}
.cover-finder .cover-results.busy * {
cursor: wait !important;
pointer-events: none;
}
.cover-container {
padding-top: 133.33%;
position: relative;
}
.cover-container.result {
cursor: pointer;
}
.spinner-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.cover-container img {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
td {
padding: 0 0.5em;
}
.env-table td {
padding: 0.25em;
border-bottom: rgba(0, 0, 0, 0.25) 1px solid;
vertical-align: top;
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<div class="my-4">
<h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div>
</div>
<div class="card p-4">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(app,i) in apps" :key="i">
<td>{{app.name}}</td>
<td>
<button class="btn btn-primary mx-1" @click="editApp(i)">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-danger mx-1" @click="showDeleteForm(i)">
<i class="fas fa-trash"></i> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="edit-form card mt-2" v-if="showEditForm">
<div class="p-4">
<!--name-->
<div class="mb-3">
<label for="appName" class="form-label">Application Name</label>
<input type="text" class="form-control" id="appName" aria-describedby="appNameHelp" v-model="editForm.name" />
<div id="appNameHelp" class="form-text">
Application Name, as shown on Moonlight
</div>
</div>
<!--output-->
<div class="mb-3">
<label for="appOutput" class="form-label">Output</label>
<input type="text" class="form-control monospace" id="appOutput" aria-describedby="appOutputHelp"
v-model="editForm.output" />
<div id="appOutputHelp" class="form-text">
The file where the output of the command is stored, if it is not
specified, the output is ignored
</div>
</div>
<!--prep-cmd-->
<div class="mb-3">
<label for="excludeGlobalPrep" class="form-label">Global Prep Commands</label>
<select id="excludeGlobalPrep" class="form-select" v-model="editForm['exclude-global-prep-cmd']">
<option v-for="val in [false, true]" :value="val">
{{ !val ? 'Enabled' : 'Disabled' }}
</option>
</select>
<div class="form-text">
Enable/Disable the execution of Global Prep Commands for this
application.
</div>
</div>
<div class="mb-3">
<label for="appName" class="form-label">Command Preparations</label>
<div class="form-text">
A list of commands to be run before/after this application.<br />
If any of the prep-commands fail, starting the application is aborted.
</div>
<div class="d-flex justify-content-start mb-3 mt-3" v-if="editForm['prep-cmd'].length === 0">
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus mr-1"></i> Add Commands
</button> </button>
<button class="btn btn-danger" @click="showDeleteForm(i)"> </div>
<i class="fas fa-trash"></i> Delete <table class="table" v-if="editForm['prep-cmd'].length > 0">
<thead>
<tr>
<th scope="col"><i class="fas fa-play"></i> Do Command</th>
<th scope="col"><i class="fas fa-undo"></i> Undo Command</th>
<th scope="col" v-if="platform === 'windows'">
<i class="fas fa-shield-alt"></i> Run as Admin
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(c, i) in editForm['prep-cmd']">
<td>
<input type="text" class="form-control monospace" v-model="c.do" />
</td>
<td>
<input type="text" class="form-control monospace" v-model="c.undo" />
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input type="checkbox" class="form-check-input" :id="'prep-cmd-admin-' + i" v-model="c.elevated"
true-value="true" false-value="false" />
<label :for="'prep-cmd-admin-' + i" class="form-check-label">Elevated</label>
</div>
</td>
<td>
<button class="btn btn-danger" @click="editForm['prep-cmd'].splice(i,1)">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!--detached-->
<div class="mb-3">
<label for="appName" class="form-label">Detached Commands</label>
<div v-for="(c,i) in editForm.detached" class="d-flex justify-content-between my-2">
<input type="text" v-model="editForm.detached[i]" class="form-control monospace">
<button class="btn btn-danger mx-2" @click="editForm.detached.splice(i,1)">
&times;
</button> </button>
</td> </div>
</tr> <div class="d-flex justify-content-between">
</tbody> <button class="btn btn-success" @click="editForm.detached.push('');">
</table> <i class="fas fa-plus mr-1"></i> Add Detached Command
</div> </button>
<div class="edit-form card mt-2" v-if="showEditForm"> </div>
<div class="p-4"> <div class="form-text">
<!--name--> A list of commands to be run and forgotten about
<div class="mb-3"> </div>
<label for="appName" class="form-label">Application Name</label>
<input
type="text"
class="form-control"
id="appName"
aria-describedby="appNameHelp"
v-model="editForm.name"
/>
<div id="appNameHelp" class="form-text">
Application Name, as shown on Moonlight
</div> </div>
</div> <!--command-->
<!--output--> <div class="mb-3">
<div class="mb-3"> <label for="appCmd" class="form-label">Command</label>
<label for="appOutput" class="form-label">Output</label> <input type="text" class="form-control monospace" id="appCmd" aria-describedby="appCmdHelp"
<input v-model="editForm.cmd" />
type="text" <div id="appCmdHelp" class="form-text">
class="form-control monospace" The main application, if it is not specified, a process is started
id="appOutput" that sleeps indefinitely
aria-describedby="appOutputHelp" </div>
v-model="editForm.output"
/>
<div id="appOutputHelp" class="form-text">
The file where the output of the command is stored, if it is not
specified, the output is ignored
</div> </div>
</div> <!--working dir-->
<!--prep-cmd--> <div class="mb-3">
<div class="mb-3"> <label for="appWorkingDir" class="form-label">Working Directory</label>
<label for="excludeGlobalPrep" class="form-label" <input type="text" class="form-control monospace" id="appWorkingDir" aria-describedby="appWorkingDirHelp"
>Global Prep Commands</label v-model="editForm['working-dir']" />
> <div id="appWorkingDirHelp" class="form-text">
<select The working directory that should be passed to the process. For
id="excludeGlobalPrep" example, some applications use the working directory to search for
class="form-select" configuration files. If not set, Sunshine will default to the parent
v-model="editForm['exclude-global-prep-cmd']" directory of the command
> </div>
<option v-for="val in [false, true]" :value="val">
{{ !val ? 'Enabled' : 'Disabled' }}
</option>
</select>
<div class="form-text">
Enable/Disable the execution of Global Prep Commands for this
application.
</div> </div>
</div> <!-- elevation -->
<div class="mb-3"> <div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="appName" class="form-label">Command Preparations</label> <label for="appElevation" class="form-check-label">Run as administrator</label>
<div class="form-text"> <input type="checkbox" class="form-check-input" id="appElevation" v-model="editForm.elevated"
A list of commands to be run before/after this application.<br /> true-value="true" false-value="false" />
If any of the prep-commands fail, starting the application is aborted. <div class="form-text">
This can be necessary for some applications that require administrator
permissions to run properly.
</div>
</div> </div>
<div <!-- auto-detach -->
class="d-flex justify-content-start mb-3 mt-3" <div class="mb-3 form-check">
v-if="editForm['prep-cmd'].length === 0" <label for="autoDetach" class="form-check-label">Continue streaming if the application exits quickly</label>
> <input type="checkbox" class="form-check-input" id="autoDetach" v-model="editForm['auto-detach']"
<button class="btn btn-success" @click="addPrepCmd"> true-value="true" false-value="false" />
<i class="fas fa-plus mr-1"></i> Add Commands <div class="form-text">
</button> This will attempt to automatically detect launcher-type apps that close
quickly after launching another program or instance of themselves. When
a launcher-type app is detected, it is treated as a detached app.
</div>
</div> </div>
<table class="table" v-if="editForm['prep-cmd'].length > 0"> <div class="mb-3">
<thead> <label for="appImagePath" class="form-label">Image</label>
<tr> <div class="input-group dropup">
<th scope="col"><i class="fas fa-play"></i> Do Command</th> <input type="text" class="form-control monospace" id="appImagePath" aria-describedby="appImagePathHelp"
<th scope="col"><i class="fas fa-undo"></i> Undo Command</th> v-model="editForm['image-path']" />
<th scope="col" v-if="platform === 'windows'"> <button class="btn btn-secondary dropdown-toggle" type="button" id="findCoverToggle"
<i class="fas fa-shield-alt"></i> Run as Admin aria-expanded="false" @click="showCoverFinder" ref="coverFinderDropdown">
</th> Find Cover
<th scope="col"></th> </button>
</tr> <div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
</thead> aria-labelledby="findCoverToggle">
<tbody> <div class="modal-header px-2">
<tr v-for="(c, i) in editForm['prep-cmd']"> <h4 class="modal-title">Covers Found</h4>
<td> <button type="button" class="btn-close mr-2" aria-label="Close" @click="closeCoverFinder"></button>
<input </div>
type="text" <div class="modal-body cover-results px-3 pt-3" :class="{ busy: coverFinderBusy }">
class="form-control monospace" <div class="row">
v-model="c.do" <div v-if="coverSearching" class="col-12 col-sm-6 col-lg-4 mb-3">
/> <div class="cover-container">
</td> <div class="spinner-border" role="status">
<td> <span class="visually-hidden">Loading...</span>
<input </div>
type="text"
class="form-control monospace"
v-model="c.undo"
/>
</td>
<td v-if="platform === 'windows'">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="'prep-cmd-admin-' + i"
v-model="c.elevated"
true-value="true"
false-value="false"
/>
<label :for="'prep-cmd-admin-' + i" class="form-check-label"
>Elevated</label
>
</div>
</td>
<td>
<button
class="btn btn-danger"
@click="$delete(editForm['prep-cmd'], i)"
>
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-success" @click="addPrepCmd">
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!--detatched-->
<div class="mb-3">
<label for="appName" class="form-label">Detached Commands</label>
<div
v-for="(c,i) in editForm.detached"
class="d-flex justify-content-between my-2"
>
<pre>{{c}}</pre>
<button
class="btn btn-danger mx-2"
@click="editForm.detached.splice(i,1)"
>
&times;
</button>
</div>
<div class="d-flex justify-content-between">
<input
type="text"
class="form-control monospace"
v-model="detachedCmd"
/>
<button
class="btn btn-success mx-2"
@click="editForm.detached.push(detachedCmd);detachedCmd = '';"
>
+
</button>
</div>
<div class="form-text">
A list of commands to be run and forgotten about
</div>
</div>
<!--command-->
<div class="mb-3">
<label for="appCmd" class="form-label">Command</label>
<input
type="text"
class="form-control monospace"
id="appCmd"
aria-describedby="appCmdHelp"
v-model="editForm.cmd"
/>
<div id="appCmdHelp" class="form-text">
The main application, if it is not specified, a process is started
that sleeps indefinitely
</div>
</div>
<!--working dir-->
<div class="mb-3">
<label for="appWorkingDir" class="form-label">Working Directory</label>
<input
type="text"
class="form-control monospace"
id="appWorkingDir"
aria-describedby="appWorkingDirHelp"
v-model="editForm['working-dir']"
/>
<div id="appWorkingDirHelp" class="form-text">
The working directory that should be passed to the process. For
example, some applications use the working directory to search for
configuration files. If not set, Sunshine will default to the parent
directory of the command
</div>
</div>
<!-- elevation -->
<div class="mb-3 form-check" v-if="platform === 'windows'">
<label for="appElevation" class="form-check-label"
>Run as administrator</label
>
<input
type="checkbox"
class="form-check-input"
id="appElevation"
v-model="editForm.elevated"
true-value="true"
false-value="false"
/>
<div class="form-text">
This can be necessary for some applications that require administrator
permissions to run properly.
</div>
</div>
<!-- auto-detach -->
<div class="mb-3 form-check">
<label for="autoDetach" class="form-check-label"
>Continue streaming if the application exits quickly</label
>
<input
type="checkbox"
class="form-check-input"
id="autoDetach"
v-model="editForm['auto-detach']"
true-value="true"
false-value="false"
/>
<div class="form-text">
This will attempt to automatically detect launcher-type apps that close
quickly after launching another program or instance of themselves. When
a launcher-type app is detected, it is treated as a detached app.
</div>
</div>
<!-- Image path -->
<div class="mb-3">
<label for="appImagePath" class="form-label">Image</label>
<div class="input-group dropup">
<input
type="text"
class="form-control monospace"
id="appImagePath"
aria-describedby="appImagePathHelp"
v-model="editForm['image-path']"
/>
<button
class="btn btn-secondary dropdown-toggle"
type="button"
id="findCoverToggle"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
v-dropdown-show="showCoverFinder"
ref="coverFinderDropdown"
>
Find Cover
</button>
<div
class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle"
>
<div class="modal-header">
<h4 class="modal-title">Covers Found</h4>
<button
type="button"
class="btn-close"
aria-label="Close"
@click="closeCoverFinder"
></button>
</div>
<div
class="modal-body cover-results px-3 pt-3"
:class="{ busy: coverFinderBusy }"
>
<div class="row">
<div
v-if="coverSearching"
class="col-12 col-sm-6 col-lg-4 mb-3"
>
<div class="cover-container">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</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"
<div @click="useCover(cover)">
v-for="(cover,i) in coverCandidates" <div class="cover-container result">
:key="'i'" <img class="rounded" :src="cover.url" />
class="col-12 col-sm-6 col-lg-4 mb-3" </div>
@click="useCover(cover)" <label class="d-block text-nowrap text-center text-truncate">
> {{cover.name}}
<div class="cover-container result"> </label>
<img class="rounded" :src="cover.url" />
</div> </div>
<label class="d-block text-nowrap text-center text-truncate">
{{cover.name}}
</label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="appImagePathHelp" class="form-text">
Application icon/picture/image path that will be sent to client. Image
must be a PNG file. If not set, Sunshine will send default box image.
</div>
</div> </div>
<div id="appImagePathHelp" class="form-text"> <div class="env-hint alert alert-info">
Application icon/picture/image path that will be sent to client. Image <div class="form-text">
must be a PNG file. If not set, Sunshine will send default box image. <h4>About Environment Variables</h4>
All commands get these environment variables by default:
</div>
<table class="env-table">
<tr>
<td><b>Var Name</b></td>
<td><b></b></td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_ID</td>
<td>App ID</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_NAME</td>
<td>App Name</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td>
<td>The Width requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td>
<td>The Height requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td>
<td>The FPS requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td>
<td>(true/false) if HDR is enabled by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td>
<td>(int) the requested gamepad mask, in a bitset/bitfield format</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td>
<td>(true/false) if the client has requested host audio</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td>
<td>(true/false) if the client has requested the option to optimize the game for optimal
streaming</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td>
<td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td>
</tr>
</table>
<div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution
Automation:</b>
<pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre>
</div>
<div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution
Automation:</b>
<pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre>
</div>
<div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for
Resolution
Automation:</b>
<pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre>
</div>
<div class="form-text"><a
href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html"
target="_blank">See More</a></div>
</div>
<!--buttons-->
<div class="d-flex">
<button @click="showEditForm = false" class="btn btn-secondary m-2">
Cancel
</button>
<button class="btn btn-primary m-2" @click="save">Save</button>
</div> </div>
</div>
<div class="env-hint">
<div class="form-text"><b>About Environment Variables: </b> All commands get these environment variables by default: </div>
<table>
<tr><td><b>Var Name</b></td><td><b></b></td></tr>
<tr><td style="font-family: monospace">SUNSHINE_APP_ID</td><td>App ID</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_APP_NAME</td><td>App Name</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td><td>The Width requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td><td>The Height requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td><td>The FPS requested by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td><td>(true/false) if HDR is enabled by the client</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td><td>(int) the requested gamepad mask, in a bitset/bitfield format</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td><td>(true/false) if the client has requested host audio</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td><td>(true/false) if the client has requested the option to optimize the game for optimal streaming</td></tr>
<tr><td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td><td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td></tr>
</table>
<div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution Automation:</b> <pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre></div>
<div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution Automation:</b> <pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre></div>
<div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for Resolution Automation:</b> <pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre></div>
<div class="form-text"><a href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html" target="_blank">See More</a></div>
</div>
<!--buttons-->
<div class="d-flex">
<button @click="showEditForm = false" class="btn btn-secondary m-2">
Cancel
</button>
<button class="btn btn-primary m-2" @click="save">Save</button>
</div> </div>
</div> </div>
<div class="mt-2" v-else>
<button class="btn btn-primary" @click="newApp">
<i class="fas fa-plus"></i> Add New
</button>
</div>
</div> </div>
<div class="mt-2" v-else> </body>
<button class="btn btn-primary" @click="newApp"> <script type="module">
<i class="fas fa-plus"></i> Add New import { createApp } from 'vue';
</button> import Navbar from './Navbar.vue'
</div> import {Dropdown} from 'bootstrap'
</div> const app = createApp({
<script> components: {
Vue.directive('dropdown-show', { Navbar
bind: function (el, binding) { },
el.addEventListener('show.bs.dropdown', binding.value);
}
});
new Vue({
el: "#app",
data() { data() {
return { return {
apps: [], apps: [],
@ -384,7 +397,7 @@
console.log(r); console.log(r);
this.apps = r.apps; this.apps = r.apps;
}); });
fetch("/api/config") fetch("/api/config")
.then(r => r.json()) .then(r => r.json())
.then(r => this.platform = r.platform); .then(r => this.platform = r.platform);
@ -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;
}, },
@ -439,8 +452,8 @@
undo: "" undo: ""
}; };
if(this.platform === 'windows'){ if (this.platform === 'windows') {
template = {...template, elevated: false}; template = { ...template, elevated: false };
} }
this.editForm["prep-cmd"].push(template); this.editForm["prep-cmd"].push(template);
@ -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>

File diff suppressed because it is too large Load diff

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,93 +1,81 @@
<div id="content" class="container"> <!DOCTYPE html>
<h1 class="my-4">Hello, Sunshine!</h1> <html lang="en">
<p>Sunshine is a self-hosted game stream host for Moonlight.</p>
<div class="alert alert-danger" v-if="fancyLogs.find(x => x.level === 'Fatal')">
<div style="line-height: 32px;">
<i class="fas fa-circle-exclamation" style="font-size: 32px;margin-right: 0.25em;"></i>
<b>Attention!</b> Sunshine detected these errors during startup. These errors <b>MUST</b> be fixed before using
Sunshine.<br>
</div>
<ul>
<li v-for="v in fancyLogs.filter(x => x.level === 'Fatal')">{{v.value}}</li>
</ul>
</div>
<!--Version-->
<div class="card p-2 my-4">
<div class="card-body" v-if="version">
<h2>Version {{version}}</h2>
<br />
<div v-if="loading">
Loading Latest Release...
</div>
<div class="alert alert-success" v-if="buildVersionIsDirty">
Thank you for helping to make Sunshine a better software! 🌇
</div>
<div v-else-if="!nightlyBuildAvailable && !stableBuildAvailable && !buildVersionIsDirty">
<div class="alert alert-success">
You're running the latest version of Sunshine
</div>
</div>
<div v-if="nightlyBuildAvailable">
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<div class="my-2">A new <b>Nightly</b> Version is Available!</div>
<a class="btn btn-success m-1" href="https://github.com/LizardByte/Sunshine/releases/nightly-dev"
target="_blank">Download</a>
</div>
<pre><b>{{nightlyData.head_sha}}</b></pre>
<pre>{{nightlyData.display_title}}</pre>
</div>
</div>
<div v-if="stableBuildAvailable">
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<div class="my-2">A new <b>Stable</b> Version is Available!</div>
<a class="btn btn-success m-1" :href="githubVersion.html_url" target="_blank">Download</a>
</div>
<h3>{{githubVersion.name}}</h3>
<pre>{{githubVersion.body}}</pre>
</div>
</div>
</div>
</div>
<!--Resources-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Resources</h2>
<br />
<p>
Resources for Sunshine!
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-success m-1" href="https://app.lizardbyte.dev" target="_blank">LizardByte Website</a>
<a class="btn btn-primary m-1" href="https://app.lizardbyte.dev/discord" target="_blank">
<i class="fab fa-fw fa-discord"></i> Discord</a>
<a class="btn btn-secondary m-1" href="https://github.com/LizardByte/Sunshine/discussions" target="_blank">
<i class="fab fa-fw fa-github"></i> Github Discussions</a>
</div>
</div>
</div>
<!--Legal-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Legal</h2>
<br />
<p>
By continuing to use this software you agree to the terms and conditions in the following documents.
</p>
<div class="card-group p-4 align-items-center">
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/LICENSE" target="_blank">
<i class="fas fa-fw fa-file-alt"></i> License</a>
<a class="btn btn-danger m-1" href="https://github.com/LizardByte/Sunshine/blob/master/NOTICE" target="_blank">
<i class="fas fa-fw fa-exclamation"></i> Third Party Notice</a>
</div>
</div>
</div>
</div>
<script> <head>
new Vue({ <%- header %>
el: "#content", </head>
<body id="app">
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">Hello, Sunshine!</h1>
<p>Sunshine is a self-hosted game stream host for Moonlight.</p>
<div class="alert alert-danger" v-if="fancyLogs.find(x => x.level === 'Fatal')">
<div style="line-height: 32px;">
<i class="fas fa-circle-exclamation" style="font-size: 32px;margin-right: 0.25em;"></i>
<b>Attention!</b> Sunshine detected these errors during startup. These errors <b>MUST</b> be fixed before using
Sunshine.<br>
</div>
<ul>
<li v-for="v in fancyLogs.filter(x => x.level === 'Fatal')">{{v.value}}</li>
</ul>
</div>
<!--Version-->
<div class="card p-2 my-4">
<div class="card-body" v-if="version">
<h2>Version {{version}}</h2>
<br />
<div v-if="loading">
Loading Latest Release...
</div>
<div class="alert alert-success" v-if="buildVersionIsDirty">
Thank you for helping to make Sunshine a better software! 🌇
</div>
<div v-else-if="!nightlyBuildAvailable && !stableBuildAvailable && !buildVersionIsDirty">
<div class="alert alert-success">
You're running the latest version of Sunshine
</div>
</div>
<div v-if="nightlyBuildAvailable">
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<div class="my-2">A new <b>Nightly</b> Version is Available!</div>
<a class="btn btn-success m-1" href="https://github.com/LizardByte/Sunshine/releases/nightly-dev"
target="_blank">Download</a>
</div>
<pre><b>{{nightlyData.head_sha}}</b></pre>
<pre>{{nightlyData.display_title}}</pre>
</div>
</div>
<div v-if="stableBuildAvailable">
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<div class="my-2">A new <b>Stable</b> Version is Available!</div>
<a class="btn btn-success m-1" :href="githubVersion.html_url" target="_blank">Download</a>
</div>
<h3>{{githubVersion.name}}</h3>
<pre>{{githubVersion.body}}</pre>
</div>
</div>
</div>
</div>
<!--Resources-->
<div class="my-4">
<Resource-Card></Resource-Card>
</div>
</div>
</body>
<script type="module">
import { createApp } from 'vue'
import Navbar from './Navbar.vue'
import ResourceCard from './ResourceCard.vue'
console.log("Hello, Sunshine!")
let app = createApp({
components: {
Navbar,
ResourceCard
},
data() { data() {
return { return {
version: null, version: null,
@ -165,4 +153,5 @@
} }
} }
}); });
app.mount('#app');
</script> </script>

View file

@ -1,85 +1,81 @@
<div id="app" class="container"> <!DOCTYPE html>
<h1 class="my-4">Password Change</h1> <html lang="en">
<form @submit.prevent="save">
<div class="card d-flex p-4 flex-row"> <head>
<div class="col-md-6 px-4"> <%- header %>
<h4>Current Credentials</h4> <style>
<div class="mb-3"> .config-page {
<label for="currentUsername" class="form-label">Username</label> padding: 1em;
<input border: 1px solid #dee2e6;
required border-top: none;
type="text" }
class="form-control"
id="currentUsername" .buttons {
v-model="passwordData.currentUsername" padding: 1em 0;
/> }
<div class="form-text">&nbsp;</div> </style>
</div> </head>
<div class="mb-3">
<label for="currentPassword" class="form-label">Password</label> <body id="app">
<input <Navbar></Navbar>
autocomplete="current-password" <div class="container">
type="password" <h1 class="my-4">Password Change</h1>
class="form-control" <form @submit.prevent="save">
id="currentPassword" <div class="card d-flex p-4 flex-row">
v-model="passwordData.currentPassword" <div class="col-md-6 px-4">
/> <h4>Current Credentials</h4>
</div> <div class="mb-3">
</div> <label for="currentUsername" class="form-label">Username</label>
<div class="col-md-6 px-4"> <input required type="text" class="form-control" id="currentUsername"
<h4>New Credentials</h4> v-model="passwordData.currentUsername" />
<div class="mb-3"> <div class="form-text">&nbsp;</div>
<label for="newUsername" class="form-label">New Username</label> </div>
<input <div class="mb-3">
type="text" <label for="currentPassword" class="form-label">Password</label>
class="form-control" <input autocomplete="current-password" type="password" class="form-control" id="currentPassword"
id="newUsername" v-model="passwordData.currentPassword" />
v-model="passwordData.newUsername"
/>
<div class="form-text">
If not specified, the username will not change
</div> </div>
</div> </div>
<div class="mb-3"> <div class="col-md-6 px-4">
<label for="newPassword" class="form-label">Password</label> <h4>New Credentials</h4>
<input <div class="mb-3">
autocomplete="new-password" <label for="newUsername" class="form-label">New Username</label>
required <input type="text" class="form-control" id="newUsername" v-model="passwordData.newUsername" />
type="password" <div class="form-text">
class="form-control" If not specified, the username will not change
id="newPassword" </div>
v-model="passwordData.newPassword" </div>
/> <div class="mb-3">
</div> <label for="newPassword" class="form-label">Password</label>
<div class="mb-3"> <input autocomplete="new-password" required type="password" class="form-control" id="newPassword"
<label for="confirmNewPassword" class="form-label" v-model="passwordData.newPassword" />
>Confirm Password</label </div>
> <div class="mb-3">
<input <label for="confirmNewPassword" class="form-label">Confirm Password</label>
autocomplete="new-password" <input autocomplete="new-password" required type="password" class="form-control" id="confirmNewPassword"
required v-model="passwordData.confirmNewPassword" />
type="password" </div>
class="form-control"
id="confirmNewPassword"
v-model="passwordData.confirmNewPassword"
/>
</div> </div>
</div> </div>
</div> <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> <div class="alert alert-success" v-if="success">
<div class="alert alert-success" v-if="success"> <b>Success! </b>This page will reload soon, your browser will ask you for
<b>Success! </b>This page will reload soon, your browser will ask you for the new credentials
the new credentials </div>
</div> <div class="mb-3 buttons">
<div class="mb-3 buttons"> <button class="btn btn-primary">Save</button>
<button class="btn btn-primary">Save</button> </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,25 +1,39 @@
<div id="content" class="container"> <!DOCTYPE html>
<h1 class="my-4">PIN Pairing</h1> <html lang="en">
<form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4">
<input
type="number"
placeholder="PIN"
id="pin-input"
class="form-control my-4"
/>
<button class="btn btn-primary">Send</button>
</div>
<div class="alert alert-warning">
<b>Warning!</b> Make sure you have access to the client you are pairing
with.<br />
This software can give total control to your computer, so be careful!
</div>
<div id="status"></div>
</form>
</div>
<script> <head>
<%- header %>
</head>
<body id="app">
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">PIN Pairing</h1>
<form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4">
<input type="text" pattern="\d*" placeholder="PIN" id="pin-input" class="form-control my-4" />
<button class="btn btn-primary">Send</button>
</div>
<div class="alert alert-warning">
<b>Warning!</b> Make sure you have access to the client you are pairing
with.<br />
This software can give total control to your computer, so be careful!
</div>
<div id="status"></div>
</form>
</div>
</body>
<script type="module">
import Navbar from './Navbar.vue'
import {createApp} from 'vue'
let app = createApp({
components: {
Navbar
}
});
app.mount("#app");
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

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 650 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before After
Before After

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,190 +1,209 @@
<div id="app" class="container"> <!DOCTYPE html>
<h1 class="my-4">Troubleshooting</h1> <html lang="en">
<!--Force Close App-->
<div class="card p-2 my-4"> <head>
<div class="card-body"> <%- header %>
<h2>Force Close</h2> <style>
<br /> .troubleshooting-logs {
<p> white-space: pre;
If Moonlight complains about an app currently running, force closing the font-family: monospace;
app should fix the issue. overflow: auto;
</p> max-height: 500px;
<div class="alert alert-success" v-if="closeAppStatus === true"> min-height: 500px;
Application Closed Successfully! font-size: 16px;
</div> position: relative;
<div class="alert alert-danger" v-if="closeAppStatus === false"> }
Error while closing Application
</div> .copy-icon {
<div> position: absolute;
<button class="btn btn-warning" :disabled="closeAppPressed" @click="closeApp"> top: 8px;
Force Close right: 8px;
</button> padding: 8px;
cursor: pointer;
color: rgba(0, 0, 0, 1);
appearance: none;
border: none;
background: none;
}
.copy-icon:hover {
color: rgba(0, 0, 0, 0.75);
}
.copy-icon:active {
color: rgba(0, 0, 0, 1);
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<h1 class="my-4">Troubleshooting</h1>
<!--Force Close App-->
<div class="card p-2 my-4">
<div class="card-body">
<h2>Force Close</h2>
<br />
<p>
If Moonlight complains about an app currently running, force closing the
app should fix the issue.
</p>
<div class="alert alert-success" v-if="closeAppStatus === true">
Application Closed Successfully!
</div>
<div class="alert alert-danger" v-if="closeAppStatus === false">
Error while closing Application
</div>
<div>
<button class="btn btn-warning" :disabled="closeAppPressed" @click="closeApp">
Force Close
</button>
</div>
</div> </div>
</div> </div>
</div> <!--Restart Sunshine-->
<!--Restart Sunshine--> <div class="card p-2 my-4">
<div class="card p-2 my-4"> <div class="card-body">
<div class="card-body"> <h2>Restart Sunshine</h2>
<h2>Restart Sunshine</h2> <br />
<br /> <p>
<p> If Sunshine isn't working properly, you can try restarting it.
If Sunshine isn't working properly, you can try restarting it. This will terminate any running sessions.
This will terminate any running sessions. </p>
</p> <div class="alert alert-success" v-if="restartPressed === true">
<div class="alert alert-success" v-if="restartPressed === true"> Sunshine is restarting
Sunshine is restarting </div>
</div> <div>
<div> <button class="btn btn-warning" :disabled="restartPressed" @click="restart">
<button class="btn btn-warning" :disabled="restartPressed" @click="restart"> Restart Sunshine
Restart Sunshine </button>
</button> </div>
</div> </div>
</div> </div>
</div> <!--Unpair all Clients-->
<!--Unpair all Clients--> <div class="card p-2 my-4">
<div class="card p-2 my-4"> <div class="card-body">
<div class="card-body"> <h2>Unpair All Clients</h2>
<h2>Unpair All Clients</h2> <br />
<br /> <p>Remove all your paired devices</p>
<p>Remove all your paired devices</p> <div class="alert alert-success" v-if="unpairAllStatus === true">
<div class="alert alert-success" v-if="unpairAllStatus === true"> Unpair Successful!
Unpair Successful! </div>
</div> <div class="alert alert-danger" v-if="unpairAllStatus === false">
<div class="alert alert-danger" v-if="unpairAllStatus === false"> Error while unpairing
Error while unpairing </div>
</div> <div>
<div> <button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll"> Unpair All
Unpair All </button>
</button> </div>
</div> </div>
</div> </div>
</div> <!--Logs-->
<!--Logs--> <div class="card p-2 my-4">
<div class="card p-2 my-4"> <div class="card-body">
<div class="card-body"> <h2>Logs</h2>
<h2>Logs</h2> <br />
<br /> <div class="d-flex justify-content-between align-items-baseline py-2">
<div class="d-flex justify-content-between align-items-baseline py-2"> <p>See the logs uploaded by Sunshine</p>
<p>See the logs uploaded by Sunshine</p> <input type="text" class="form-control" v-model="logFilter" placeholder="Find..." style="width: 300px">
<input type="text" class="form-control" v-model="logFilter" placeholder="Find..." style="width: 300px"> </div>
</div> <div>
<div> <div class="troubleshooting-logs">
<div class="troubleshooting-logs"> <button class="copy-icon"><i class="fas fa-copy " @click="copyLogs"></i></button>{{actualLogs}}
<button class="copy-icon"><i class="fas fa-copy " @click="copyLogs"></i></button>{{actualLogs}} </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<script> <script type="module">
new Vue({ import { createApp } from 'vue'
el: "#app", import Navbar from './Navbar.vue'
data() {
return { const app = createApp({
closeAppPressed: false, components: {
closeAppStatus: null, Navbar
unpairAllPressed: false, },
unpairAllStatus: null, data() {
restartPressed: false, return {
logs: 'Loading...', closeAppPressed: false,
logFilter: null, closeAppStatus: null,
logInterval: null, unpairAllPressed: false,
}; unpairAllStatus: null,
}, restartPressed: false,
computed: { logs: 'Loading...',
actualLogs(){ logFilter: null,
if(!this.logFilter)return this.logs; logInterval: null,
let lines = this.logs.split("\n"); };
lines = lines.filter(x => x.indexOf(this.logFilter) != -1); },
return lines.join("\n"); computed: {
} actualLogs() {
}, if (!this.logFilter) return this.logs;
created() { let lines = this.logs.split("\n");
this.logInterval = setInterval(() => { lines = lines.filter(x => x.indexOf(this.logFilter) != -1);
return lines.join("\n");
}
},
created() {
this.logInterval = setInterval(() => {
this.refreshLogs();
}, 5000);
this.refreshLogs(); this.refreshLogs();
}, 5000);
this.refreshLogs();
},
beforeDestroy(){
clearInterval(this.logInterval);
},
methods: {
refreshLogs() {
fetch("/api/logs",)
.then((r) => r.text())
.then((r) => {
this.logs = r;
});
}, },
closeApp() { beforeDestroy() {
this.closeAppPressed = true; clearInterval(this.logInterval);
fetch("/api/apps/close", { method: "POST" })
.then((r) => r.json())
.then((r) => {
this.closeAppPressed = false;
this.closeAppStatus = r.status.toString() === "true";
setTimeout(() => {
this.closeAppStatus = null;
}, 5000);
});
}, },
unpairAll() { methods: {
this.unpairAllPressed = true; refreshLogs() {
fetch("/api/clients/unpair", { method: "POST" }) fetch("/api/logs",)
.then((r) => r.json()) .then((r) => r.text())
.then((r) => { .then((r) => {
this.unpairAllPressed = false; this.logs = r;
this.unpairAllStatus = r.status.toString() === "true"; });
setTimeout(() => { },
this.unpairAllStatus = null; closeApp() {
}, 5000); this.closeAppPressed = true;
}); fetch("/api/apps/close", { method: "POST" })
}, .then((r) => r.json())
copyLogs(){ .then((r) => {
navigator.clipboard.writeText(this.actualLogs); this.closeAppPressed = false;
}, this.closeAppStatus = r.status.toString() === "true";
restart() { setTimeout(() => {
this.restartPressed = true; this.closeAppStatus = null;
setTimeout(() => { }, 5000);
});
},
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair", { method: "POST" })
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
});
},
copyLogs() {
navigator.clipboard.writeText(this.actualLogs);
},
restart() {
this.restartPressed = true;
setTimeout(() => {
this.restartPressed = false; this.restartPressed = false;
}, 5000); }, 5000);
fetch("/api/restart", { fetch("/api/restart", {
method: "POST", method: "POST",
}); });
},
}, },
}, });
});
</script>
<style> app.mount("#app");
.troubleshooting-logs { </script>
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 { </body>
color: rgba(0,0,0,0.75);
}
.copy-icon:active {
color: rgba(0,0,0,1);
}
</style>

View file

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