Web UI migration to Vite and Vue3 and improvements to the UX (#1673)
Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
60
src_assets/common/assets/web/Navbar.vue
Normal 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>
|
||||
36
src_assets/common/assets/web/ResourceCard.vue
Normal 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>
|
||||
@@ -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)">
|
||||
×
|
||||
</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)"
|
||||
>
|
||||
×
|
||||
</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 <qres path>\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 <qres path>\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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<div id="content" class="container">
|
||||
<h1>Clients</h1>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </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"> </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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 650 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 681 B After Width: | Height: | Size: 681 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
9
src_assets/common/assets/web/template_header.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||