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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,370 +1,383 @@
<div id="app" 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" @click="editApp(i)">
<i class="fas fa-edit"></i> Edit
<!DOCTYPE html>
<html lang="en">
<head>
<%- header %>
<style>
.precmd-head {
width: 200px;
}
.monospace {
font-family: monospace;
}
.cover-finder {}
.cover-finder .cover-results {
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
.cover-finder .cover-results.busy * {
cursor: wait !important;
pointer-events: none;
}
.cover-container {
padding-top: 133.33%;
position: relative;
}
.cover-container.result {
cursor: pointer;
}
.spinner-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.cover-container img {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
td {
padding: 0 0.5em;
}
.env-table td {
padding: 0.25em;
border-bottom: rgba(0, 0, 0, 0.25) 1px solid;
vertical-align: top;
}
</style>
</head>
<body id="app">
<Navbar></Navbar>
<div class="container">
<div class="my-4">
<h1>Applications</h1>
<div>Applications are refreshed only when Client is restarted</div>
</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 class="btn btn-danger" @click="showDeleteForm(i)">
<i class="fas fa-trash"></i> Delete
</div>
<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>
</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 class="d-flex justify-content-between">
<button class="btn btn-success" @click="editForm.detached.push('');">
<i class="fas fa-plus mr-1"></i> Add Detached Command
</button>
</div>
<div class="form-text">
A list of commands to be run and forgotten about
</div>
</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
<!--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>
</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.
<!--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>
</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.
<!-- 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>
<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>
<!-- 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>
<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="$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 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"
aria-expanded="false" @click="showCoverFinder" ref="coverFinderDropdown">
Find Cover
</button>
<div class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
aria-labelledby="findCoverToggle">
<div class="modal-header px-2">
<h4 class="modal-title">Covers Found</h4>
<button type="button" class="btn-close mr-2" aria-label="Close" @click="closeCoverFinder"></button>
</div>
<div class="modal-body cover-results px-3 pt-3" :class="{ busy: coverFinderBusy }">
<div class="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
v-for="(cover,i) in coverCandidates"
:key="'i'"
class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)"
>
<div class="cover-container result">
<img class="rounded" :src="cover.url" />
<div v-for="(cover,i) in coverCandidates" :key="'i'" class="col-12 col-sm-6 col-lg-4 mb-3"
@click="useCover(cover)">
<div class="cover-container result">
<img class="rounded" :src="cover.url" />
</div>
<label class="d-block text-nowrap text-center text-truncate">
{{cover.name}}
</label>
</div>
<label class="d-block text-nowrap text-center text-truncate">
{{cover.name}}
</label>
</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 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 class="env-hint alert alert-info">
<div class="form-text">
<h4>About Environment Variables</h4>
All commands get these environment variables by default:
</div>
<table class="env-table">
<tr>
<td><b>Var Name</b></td>
<td><b></b></td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_ID</td>
<td>App ID</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_APP_NAME</td>
<td>App Name</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td>
<td>The Width requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td>
<td>The Height requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td>
<td>The FPS requested by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td>
<td>(true/false) if HDR is enabled by the client</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td>
<td>(int) the requested gamepad mask, in a bitset/bitfield format</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td>
<td>(true/false) if the client has requested host audio</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td>
<td>(true/false) if the client has requested the option to optimize the game for optimal
streaming</td>
</tr>
<tr>
<td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td>
<td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td>
</tr>
</table>
<div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution
Automation:</b>
<pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre>
</div>
<div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution
Automation:</b>
<pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre>
</div>
<div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for
Resolution
Automation:</b>
<pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre>
</div>
<div class="form-text"><a
href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html"
target="_blank">See More</a></div>
</div>
<!--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 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 class="mt-2" v-else>
<button class="btn btn-primary" @click="newApp">
<i class="fas fa-plus"></i> Add New
</button>
</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>
<script>
Vue.directive('dropdown-show', {
bind: function (el, binding) {
el.addEventListener('show.bs.dropdown', binding.value);
}
});
new Vue({
el: "#app",
</body>
<script type="module">
import { createApp } from 'vue';
import Navbar from './Navbar.vue'
import {Dropdown} from 'bootstrap'
const app = createApp({
components: {
Navbar
},
data() {
return {
apps: [],
@ -384,7 +397,7 @@
console.log(r);
this.apps = r.apps;
});
fetch("/api/config")
.then(r => r.json())
.then(r => this.platform = r.platform);
@ -408,18 +421,18 @@
},
editApp(id) {
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
this.$set(this.editForm, "index", id);
this.editForm.index = id;
if (this.editForm["prep-cmd"] === undefined)
this.$set(this.editForm, "prep-cmd", []);
this.editForm["prep-cmd"] = [];
if (this.editForm["detached"] === undefined)
this.$set(this.editForm, "detached", []);
this.editForm["detached"] = [];
if (this.editForm["exclude-global-prep-cmd"] === undefined)
this.$set(this.editForm, "exclude-global-prep-cmd", false);
if(this.editForm["elevated"] === undefined && this.platform === 'windows'){
this.$set(this.editForm, "elevated", false);
this.editForm["exclude-global-prep-cmd"] = [];
if (this.editForm["elevated"] === undefined && this.platform === 'windows') {
this.editForm["elevated"] = [];
}
if(this.editForm["auto-detach"] === undefined){
this.$set(this.editForm, "auto-detach", true);
if (this.editForm["auto-detach"] === undefined) {
this.editForm["auto-detach"] = true;
}
this.showEditForm = true;
},
@ -439,8 +452,8 @@
undo: ""
};
if(this.platform === 'windows'){
template = {...template, elevated: false};
if (this.platform === 'windows') {
template = { ...template, elevated: false };
}
this.editForm["prep-cmd"].push(template);
@ -448,7 +461,19 @@
showCoverFinder($event) {
this.coverCandidates = [];
this.coverSearching = true;
const ref = this.$refs.coverFinderDropdown;
if (!ref) {
console.error("Ref not found!");
return;
}
this.coverFinderDropdown = Dropdown.getInstance(ref);
if (!this.coverFinderDropdown) {
this.coverFinderDropdown = new Dropdown(ref);
if (!this.coverFinderDropdown) {
return;
}
}
this.coverFinderDropdown.show();
function getSearchBucket(name) {
let bucket = name.substring(0, Math.min(name.length, 2)).toLowerCase().replaceAll(/[^a-z\d]/g, '');
if (!bucket) {
@ -503,7 +528,7 @@
if (!ref) {
return;
}
const dropdown = this.coverFinderDropdown = bootstrap.Dropdown.getInstance(ref);
const dropdown = this.coverFinderDropdown = Dropdown.getInstance(ref);
if (!dropdown) {
return;
}
@ -520,7 +545,7 @@
}).then(r => {
if (!r.ok) throw new Error("Failed to download covers");
return r.json();
}).then(body => this.$set(this.editForm, "image-path", body.path))
}).then(body => this.editForm["image-path"] = body.path)
.then(() => this.closeCoverFinder())
.finally(() => this.coverFinderBusy = false);
},
@ -535,66 +560,13 @@
},
},
});
app.directive('dropdown-show', {
mounted: function (el, binding) {
el.addEventListener('show.bs.dropdown', binding.value);
}
});
app.mount("#app")
</script>
<style>
.precmd-head {
width: 200px;
}
.monospace {
font-family: monospace;
}
.cover-finder {
}
.cover-finder .cover-results {
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
.cover-finder .cover-results.busy * {
cursor: wait !important;
pointer-events: none;
}
.cover-container {
padding-top: 133.33%;
position: relative;
}
.cover-container.result {
cursor: pointer;
}
.spinner-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.cover-container img {
display: block;
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.config-page {
padding: 1em;
border: 1px solid #dee2e6;
border-top: none;
}
td {
padding: 0 0.5em;
}
</style>

View File

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

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">
<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="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>
<!DOCTYPE html>
<html lang="en">
<script>
new Vue({
el: "#content",
<head>
<%- header %>
</head>
<body id="app">
<Navbar></Navbar>
<div id="content" class="container">
<h1 class="my-4">Hello, Sunshine!</h1>
<p>Sunshine is a self-hosted game stream host for Moonlight.</p>
<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() {
return {
version: null,
@ -165,4 +153,5 @@
}
}
});
app.mount('#app');
</script>

View File

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

View File

@ -1,25 +1,39 @@
<div id="content" class="container">
<h1 class="my-4">PIN Pairing</h1>
<form action="" class="form d-flex flex-column align-items-center" id="form">
<div class="card flex-column d-flex p-4 mb-4">
<input
type="number"
placeholder="PIN"
id="pin-input"
class="form-control my-4"
/>
<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>
<!DOCTYPE html>
<html lang="en">
<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) => {
e.preventDefault();
let pin = document.querySelector("#pin-input").value;

View File

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 643 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 650 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

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

View File

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

View File

@ -1,67 +1,67 @@
<main role="main" id="app" style="max-width: 600px; margin: 0 auto">
<header>
<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>
<!DOCTYPE html>
<html lang="en">
<script>
new Vue({
el: "#app",
<head>
<%- header %>
</head>
<body id="app">
<main role="main" style="max-width: 1200px; margin: 1em auto">
<div class="d-flex gap-4">
<div class="card p-2">
<header>
<h1 class="mb-0">
<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() {
return {
error: null,
@ -101,4 +101,5 @@
},
},
});
app.mount("#app");
</script>

53
vite.config.js Normal file
View File

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