mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
1657 lines
80 KiB
HTML
1657 lines
80 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<link rel="shortcut icon" type="image/png" href="images/favicon.png"/>
|
||
<title>mStream Express Server</title>
|
||
|
||
<script src="js/materialize.min.js"></script>
|
||
<link href="css/materialize.min.css" rel="stylesheet">
|
||
|
||
<link href="css/index3.css" rel="stylesheet">
|
||
<link href="css/boot.css" rel="stylesheet">
|
||
<script src="js/vue.js"></script>
|
||
|
||
<script src="js/izi-toast.min.js"></script>
|
||
<link href="css/izi-toast.min.css" rel="stylesheet">
|
||
|
||
<link href="fonts/jura.css" rel="stylesheet">
|
||
</head>
|
||
|
||
<body>
|
||
<div class="modal">
|
||
<div id="switcherModal">
|
||
<component :key="componentKey" v-bind:is="currentViewModal">
|
||
</component>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="main-left-col">
|
||
<!-- Buttons -->
|
||
<div class="left-nav-menu-header">
|
||
Server
|
||
</div>
|
||
<ul class="left-nav-menu">
|
||
<li id="nav-directories" class="left-nav-button waves-effect waves-purple nav-selected">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" height="28"><path fill="#FFA000" d="M38 12H22l-4-4H8c-2.2 0-4 1.8-4 4v24c0 2.2 1.8 4 4 4h31c1.7 0 3-1.3 3-3V16c0-2.2-1.8-4-4-4z"/><path fill="#FFCA28" d="M42.2 18H15.3c-1.9 0-3.6 1.4-3.9 3.3L8 40h31.7c1.9 0 3.6-1.4 3.9-3.3l2.5-14c.5-2.4-1.4-4.7-3.9-4.7z"/></svg>
|
||
<span>Directories</span>
|
||
</li>
|
||
<li id="nav-users" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="200 200 800 800"><path fill="#FFF" d="M671.027 410.286c31.749 19.852 54.013 53.574 58.044 92.642 12.928 6.092 27.325 9.578 42.596 9.578 55.593 0 100.645-45.073 100.645-100.73 0-55.591-45.052-100.706-100.645-100.706-55.152.087-99.807 44.283-100.64 99.216m-65.998 206.146c55.611 0 100.749-45.093 100.749-100.705 0-55.568-45.138-100.665-100.749-100.665-55.569 0-100.662 45.097-100.662 100.665 0 55.612 45.092 100.705 100.662 100.705m42.706 6.883H562.3c-71.084 0-128.886 57.823-128.886 128.886v104.541l.218 1.623 7.232 2.255c67.816 21.213 126.781 28.225 175.295 28.225 94.746 0 149.683-26.955 153.076-28.728l6.707-3.375h.723V752.201c0-71.063-57.827-128.886-128.93-128.886M814.33 519.32h-84.754c-.965 33.965-15.406 64.487-38.305 86.465 63.217 18.8 109.431 77.392 109.431 146.613v32.211c83.705-3.089 131.951-26.823 135.152-28.442l6.707-3.42h.744V648.273c0-71.106-57.826-128.953-128.975-128.953m-385.958-6.814c19.655 0 37.997-5.742 53.556-15.537 4.907-32.188 22.172-60.344 46.827-79.498.108-1.882.283-3.726.283-5.611 0-55.631-45.094-100.704-100.666-100.704-55.633 0-100.751 45.073-100.751 100.704.001 55.573 45.118 100.646 100.751 100.646m90.433 93.279c-22.766-21.846-37.249-52.261-38.279-85.942-3.159-.216-6.248-.523-9.468-.523h-85.455c-71.083 0-128.929 57.847-128.929 128.953v104.474l.26 1.644 7.188 2.301c54.474 17.026 103.075 24.829 145.234 27.238v-31.532c-.001-69.221 46.233-127.77 109.449-146.613"/></svg>
|
||
<span>Users</span>
|
||
</li>
|
||
<li id="nav-security" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 460 460"><path d="M360.228,386.747L230,460L99.772,386.747c-27.804-15.639-45.01-45.06-45.01-76.96 V21.905L230,0l175.238,21.905v287.882C405.238,341.687,388.032,371.107,360.228,386.747z" fill="#86c867"/><path d="M335.565,357.061L230,416.442l-105.565-59.38 c-22.538-12.678-36.486-36.526-36.486-62.385V54.762L230,37.006l142.051,17.756v239.915 C372.051,320.535,358.103,344.384,335.565,357.061z" fill="#5e9b3e"/><path d="M230,460l130.228-73.253c27.803-15.64,45.01-45.06,45.01-76.96V21.905 L230,0v37.005l142.051,17.756v239.915c0,25.859-13.948,49.707-36.486,62.385L230,416.442V460z" opacity=".7" fill="#86c867"/><path d="M230,98.571c-30.244,0-54.762,24.518-54.762,54.762 c0,22.454,13.519,41.74,32.857,50.192V222.2h9.701c4.308,0,7.8,3.492,7.8,7.8s-3.492,7.8-7.8,7.8h-9.701v17.257h9.701 c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8h-9.701v17.257h9.701c4.308,0,7.8,3.492,7.8,7.8c0,4.308-3.492,7.8-7.8,7.8 h-9.701v25.057L230,339.524l21.905-10.952V203.525c19.338-8.452,32.857-27.739,32.857-50.192 C284.762,123.089,260.244,98.571,230,98.571z M230,157.167c-8.166,0-14.786-6.62-14.786-14.786c0-8.166,6.62-14.786,14.786-14.786 s14.786,6.62,14.786,14.786C244.786,150.547,238.166,157.167,230,157.167z" fill="#ecf0f1"/></svg>
|
||
<span>Security</span>
|
||
</li>
|
||
<li id="nav-federation" class="left-nav-button waves-effect waves-purple">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 48 48"><path d="M 44.398438 17.300781 C 44.300781 15.898438 43.300781 14.800781 41.898438 14.601563 C 38.398438 14 30.300781 13 24.199219 13 C 18.101563 13 9.601563 13.898438 6 14.5 C 4.699219 14.699219 3.699219 15.800781 3.5 17.199219 L 3 25 C 2.898438 26.601563 4.101563 27.800781 5.699219 28.199219 C 8.601563 29 15.699219 29 24.199219 29 C 32.699219 29 39.5 28.898438 42.300781 28.300781 C 43.898438 28 45.101563 26.699219 45 25.101563 Z M 39 22 L 9 22 C 8.398438 22 8 21.5 8 21 C 8 20.5 8.398438 20 9 20 L 39 20 C 39.601563 20 40 20.5 40 21 C 40 21.5 39.601563 22 39 22 Z" fill="#ffc107"/><path d="M 23.699219 3 C 23.5 3 23.5 3 23.398438 3.199219 C 19 8.398438 15.699219 14.800781 13.398438 22.898438 C 11.800781 28.898438 10.5 36.101563 10 44.699219 C 10 44.898438 10.199219 45 10.300781 45 L 10.5 45 C 10.699219 45 10.699219 45 10.800781 44.800781 C 15.5 38.601563 19.800781 33.699219 26 29.800781 C 27 29.300781 27.5 29 28 29 C 28.199219 29 28.5 29.199219 29 29.800781 C 31.601563 33.398438 34.398438 36.898438 37.199219 41.800781 C 37.199219 42 37.5 42 37.699219 42 C 37.898438 42 38 41.800781 38 41.699219 C 37.300781 34.5 36.199219 27.898438 33.898438 21.398438 C 31.601563 14.699219 28.300781 8.5 24.101563 3.199219 C 24.101563 3 23.898438 3 23.699219 3 Z" fill="#455a64"/><path d="M 32 22.101563 C 30 16.101563 27.199219 10.699219 23.699219 6 C 20 10.800781 17.300781 16.5 15.398438 23.398438 C 14 28.601563 13 33.898438 12.398438 39.601563 C 16.300781 34.800781 20 31.199219 25 28.101563 L 25.101563 28 L 25.199219 28 C 25.398438 27.898438 25.601563 27.800781 25.699219 27.699219 C 26.5 27.300781 27.101563 27 28 27 C 29.398438 27 30.300781 28.199219 30.601563 28.601563 C 31.101563 29.199219 31.5 29.800781 32 30.5 C 33.101563 31.898438 34.101563 33.398438 35.199219 34.898438 C 34.398438 30.199219 33.398438 26 32 22.101563 Z" fill="#c5cae9"/></svg>
|
||
<span>Federation</span>
|
||
</li>
|
||
<li id="nav-network" class="left-nav-button waves-effect waves-purple">
|
||
<svg height="28" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M490.51,294.933v-78.613l-57.766-11.093c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337 l-55.602-55.587l-48.244,32.663c-12.309-6.885-25.492-12.177-39.338-16.109l-11.043-57.14h-78.614l-11.049,57.194 C191.568,83.21,178.34,88.654,166,95.592l-48.069-32.61l-55.589,55.562l32.726,48.312c-6.765,12.204-12.277,25.253-16.142,38.959 L21.49,216.841v78.613l57.659,11.078c3.917,13.661,9.426,26.676,16.228,38.834l-32.932,48.67l55.601,55.59l48.83-33.061 c12.103,6.719,25.048,11.981,38.629,15.852l11.099,57.524h78.614l11.096-57.531c13.684-3.894,26.717-9.275,38.896-16.063 l48.832,33.101l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032L490.51,294.933 M255.975,378.017 c-67.441,0-122.115-54.673-122.115-122.114s54.674-122.115,122.115-122.115c67.441,0,122.115,54.673,122.115,122.115 S323.416,378.017,255.975,378.017" fill="#F56F6C"/><path d="M254.297,378v0.006c0.439,0.006,0.877,0.01,1.316,0.01C255.322,378.014,255.289,378.005,254.297,378 M255.676,133.789c-0.461,0-0.92,0.004-1.379,0.011v0.005C255.313,133.801,255.359,133.791,255.676,133.789 M255.975,133.788 l-0.086,0.001c67.402,0.047,122.113,54.702,122.113,122.114c0,67.405-54.744,122.055-122.135,122.114h0.107 c67.441,0,122.115-54.673,122.115-122.114S323.416,133.788,255.975,133.788 M393.775,62.645l-0.014,0.009l55.592,55.579 l-32.701,48.337c6.738,12.108,12.209,25.063,16.092,38.657l57.766,11.093v78.613v-78.613l-57.766-11.093 c-3.883-13.594-9.268-26.549-16.006-38.657l32.66-48.337L393.775,62.645 M295.127,22.058h-40.83H295.127l11.043,57.14 c13.846,3.933,27.029,9.225,39.338,16.109l0.006-0.003c-12.307-6.884-25.494-12.174-39.338-16.106L295.127,22.058" fill="#F2F2F2"/><path d="M295.127,22.058h-40.83V133.8c0.459-0.007,0.918-0.011,1.379-0.011l0.125-0.001l0.088,0.001l0.086-0.001 c67.441,0,122.115,54.673,122.115,122.115s-54.674,122.114-122.115,122.114h-0.107h-0.109l-0.145-0.001 c-0.439,0-0.877-0.004-1.316-0.01v111.936h40.92l11.096-57.531c13.684-3.894,26.717-9.275,38.896-16.063l48.832,33.101 l55.588-55.574l-33.072-48.836c6.805-12.221,12.383-25.298,16.279-39.032l57.674-11.073v-78.613l-57.766-11.093 c-3.883-13.594-9.354-26.549-16.092-38.657l32.701-48.337l-55.592-55.579l-48.244,32.654l-0.004-0.003l-0.006,0.003 c-12.309-6.885-25.492-12.177-39.338-16.109L295.127,22.058" fill="#E96966"/><path d="M256.072,111.928c-79.505,0-143.957,64.451-143.957,143.956c0,79.503,64.452,143.955,143.957,143.955 c79.504,0,143.955-64.452,143.955-143.955C400.027,176.379,335.576,111.928,256.072,111.928z M256.072,329.569 c-40.696,0-73.687-32.99-73.687-73.686c0-40.697,32.991-73.687,73.687-73.687c40.695,0,73.686,32.989,73.686,73.687 C329.758,296.579,296.768,329.569,256.072,329.569z" fill="#FFF"/></svg>
|
||
<span>Advanced</span>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="left-nav-menu-header">
|
||
Other
|
||
</div>
|
||
<ul class="left-nav-menu">
|
||
<li id="nav-auto-dns" class="left-nav-button waves-effect waves-light">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#8c9eff" d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg>
|
||
<span>mStream RPN</span>
|
||
</li>
|
||
<li id="nav-about" class="left-nav-button waves-effect waves-light">
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="28" viewBox="0 -1 24 24" fill="#fff"><path d="M4 2c-1.093 0-2 .907-2 2v18.406l1.719-1.687L6.438 18H20c1.093 0 2-.907 2-2V4c0-1.093-.907-2-2-2H4zm0 2h16v12H5.594l-.313.281L4 17.563V4zm7 2v2h2V6h-2zm0 3v5h2V9h-2z"/></svg>
|
||
<span>About</span>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- Boot Server Button -->
|
||
<div class="boot-server-button-wrapper">
|
||
<div class="boot-server-flex-wrapper">
|
||
<label class="autoboot-label">
|
||
<input id="boot-server-checkbox" type="checkbox" checked="checked" />
|
||
<span>Boot on startup</span>
|
||
</label>
|
||
<a id="boot-server-button" class="waves-effect waves-light btn blue accent-3">Boot Server</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="switcherMain" class="main-right-col">
|
||
<component v-bind:is="currentViewMain">
|
||
</component>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const remote = require('electron').remote;
|
||
const {ipcRenderer} = require('electron');
|
||
const app = remote.app;
|
||
const dialog = remote.require('electron').dialog;
|
||
const axios = require('axios');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const crypto = require('crypto')
|
||
const Login = require('../modules/login');
|
||
const shell = require('electron').shell
|
||
|
||
const apiEndpoint = 'https://api.mstream.io';
|
||
const configFile = path.join(app.getPath('userData'), 'save/server-config-v2.json');
|
||
var loadJson = {};
|
||
var editThisUser;
|
||
var bootFlag = false;
|
||
|
||
// Open a tags in OS browser
|
||
document.addEventListener('click', function (event) {
|
||
if (event.target.tagName === 'A' && event.target.href.startsWith('http')) {
|
||
event.preventDefault();
|
||
shell.openExternal(event.target.href);
|
||
}
|
||
})
|
||
|
||
try {
|
||
if (fs.statSync(configFile).isFile()) {
|
||
loadJson = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||
}
|
||
} catch(error) {
|
||
console.log('Failed To Load JSON');
|
||
}
|
||
|
||
if (!loadJson.users || typeof loadJson.users !== 'object') {
|
||
loadJson.users = {};
|
||
}
|
||
|
||
if (!loadJson.folders || typeof loadJson.users !== 'object') {
|
||
loadJson.folders = {};
|
||
}
|
||
|
||
if (!loadJson.ssl || typeof loadJson.ssl !== 'object') {
|
||
loadJson.ssl = {};
|
||
}
|
||
|
||
if (!loadJson.port) {
|
||
loadJson.port = 3000;
|
||
}
|
||
|
||
if (!loadJson.scanOptions) {
|
||
loadJson.scanOptions = {};
|
||
}
|
||
|
||
if (!loadJson.storage) {
|
||
loadJson.storage = {};
|
||
}
|
||
|
||
if (!loadJson.federation) {
|
||
loadJson.federation = {};
|
||
}
|
||
|
||
if (!loadJson.transcode) {
|
||
loadJson.transcode = {};
|
||
}
|
||
|
||
if (!loadJson.secret) {
|
||
loadJson.secret = crypto.randomBytes(Math.ceil(128/2)).toString('base64').slice(0,128);
|
||
}
|
||
|
||
if (typeof loadJson.noUpload !== 'boolean') {
|
||
loadJson.noUpload = false;
|
||
}
|
||
|
||
if (!loadJson.storage.albumArtDirectory) {
|
||
loadJson.storage.albumArtDirectory = path.join(app.getPath('userData'), 'image-cache');
|
||
}
|
||
|
||
if (!loadJson.storage.dbDirectory) {
|
||
loadJson.storage.dbDirectory = path.join(app.getPath('userData'), 'save');
|
||
}
|
||
|
||
if (!loadJson.storage.logsDirectory) {
|
||
loadJson.storage.logsDirectory = path.join(app.getPath('userData'), 'logs');
|
||
}
|
||
|
||
if (!loadJson.storage.syncConfigDirectory) {
|
||
loadJson.storage.syncConfigDirectory = path.join(app.getPath('userData'), 'sync');
|
||
}
|
||
|
||
if (!loadJson.transcode.ffmpegDirectory) {
|
||
loadJson.transcode.ffmpegDirectory = path.join(app.getPath('userData'), 'ffmpeg')
|
||
}
|
||
|
||
if (!loadJson.transcode.enabled) {
|
||
loadJson.transcode.enabled = false;
|
||
}
|
||
|
||
if (!loadJson.transcode.defaultCodec) {
|
||
loadJson.transcode.defaultCodec = 'opus';
|
||
}
|
||
|
||
if (!loadJson.transcode.defaultBitrate) {
|
||
loadJson.transcode.defaultBitrate = '96k';
|
||
}
|
||
|
||
// Auto DNS Testing
|
||
if (!loadJson.ddns) {
|
||
loadJson.ddns = {};
|
||
} else {
|
||
loadJson.ddns.tested = false;
|
||
testAutoDns();
|
||
}
|
||
|
||
async function testAutoDns() {
|
||
try {
|
||
const loginRes = await axios({
|
||
method: 'post',
|
||
url: apiEndpoint + '/login',
|
||
headers: { 'accept': 'application/json' },
|
||
responseType: 'json',
|
||
data: {
|
||
email: loadJson.ddns.email,
|
||
password: loadJson.ddns.password
|
||
}
|
||
});
|
||
loadJson.ddns.tested = true;
|
||
} catch(err) {}
|
||
}
|
||
|
||
function hashPassword(password) {
|
||
return new Promise((resolve, reject) => {
|
||
Login.hashPassword(password, (salt, hashedPassword, err) => {
|
||
if (err) {
|
||
// return callback(false, err);
|
||
return reject('Failed to hash password');
|
||
}
|
||
resolve({salt, hashPassword: Buffer.from(hashedPassword).toString('hex')});
|
||
});
|
||
});
|
||
}
|
||
|
||
window.onload = function () {
|
||
// Initialize Modal
|
||
var modalInstance = M.Modal.init(document.querySelectorAll('.modal'), { endingTop: '20%' });
|
||
|
||
Vue.component('folder-accordion', {
|
||
data: function() {
|
||
return {
|
||
instances: null,
|
||
directories: loadJson.folders,
|
||
resetFlag: false,
|
||
closeOverride: () => {
|
||
if (this.resetFlag) {
|
||
this.$parent.componentKey = !this.$parent.componentKey;
|
||
}
|
||
}
|
||
};
|
||
},
|
||
template: '\
|
||
<ul class="z-depth-1 collapsible-folders">\
|
||
<li v-for="(value, key, index) in directories">\
|
||
<div v-on:click="toggleOptions(index, $event)" class="collapsible-header">\
|
||
<div class="accordion-header-left">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="100%"><path fill="#FFA000" d="M38 12H22l-4-4H8c-2.2 0-4 1.8-4 4v24c0 2.2 1.8 4 4 4h31c1.7 0 3-1.3 3-3V16c0-2.2-1.8-4-4-4z"/><path fill="#FFCA28" d="M42.2 18H15.3c-1.9 0-3.6 1.4-3.9 3.3L8 40h31.7c1.9 0 3.6-1.4 3.9-3.3l2.5-14c.5-2.4-1.4-4.7-3.9-4.7z"/></svg>\
|
||
</div>\
|
||
<div class="accordion-header-right">\
|
||
<div><b>{{key}}</b></div>\
|
||
<div>{{value.root}}</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="collapsible-body clearfix">\
|
||
<div class="folder-button-group">\
|
||
<a v-on:click="changeDirectory(value, key, index)" class="waves-effect waves-light btn">Change Directory</a>\
|
||
<a v-on:click="deleteFolder(value, key, index)" class="waves-effect waves-light btn red">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path fill="#FFF" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\
|
||
</a>\
|
||
</div>\
|
||
</div>\
|
||
</li>\
|
||
</ul>',
|
||
mounted: function () {
|
||
this.instances = M.Collapsible.init(document.querySelectorAll('.collapsible-folders'), { onCloseEnd: this.closeOverride });
|
||
},
|
||
beforeDestroy: function() {
|
||
this.instances[0].destroy();
|
||
},
|
||
methods: {
|
||
changeDirectory: function(value, key, index) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedDirectory) => {
|
||
if(selectedDirectory.length === 0 ){
|
||
return;
|
||
}
|
||
loadJson.folders[key] = { root: selectedDirectory[0] };
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
});
|
||
},
|
||
deleteFolder: function(value, key, index) {
|
||
iziToast.question({
|
||
timeout: 20000,
|
||
close: false,
|
||
overlayClose: true,
|
||
overlay: true,
|
||
displayMode: 'once',
|
||
id: 'question',
|
||
zindex: 99999,
|
||
title: "Delete <b>'" + value.root + "'</b>?",
|
||
position: 'center',
|
||
buttons: [
|
||
['<button><b>Delete</b></button>', (instance, toast) => {
|
||
delete loadJson.folders[key];
|
||
if(loadJson.users) {
|
||
Object.keys(loadJson.users).forEach(user => {
|
||
loadJson.users[user].vpaths = loadJson.users[user].vpaths.filter(e => {
|
||
return ![key].includes(e);
|
||
});
|
||
});
|
||
}
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}, true],
|
||
['<button>Go Back</button>', (instance, toast) => {
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}],
|
||
]
|
||
});
|
||
},
|
||
toggleOptions: function(index, el) {
|
||
if(el.target.classList.contains('btn')){
|
||
return;
|
||
}
|
||
|
||
if(el.currentTarget.parentElement.classList.contains('active')){
|
||
this.instances[0].close(index);
|
||
return;
|
||
}
|
||
|
||
this.instances[0].open(index);
|
||
}
|
||
}
|
||
});
|
||
|
||
const foldersView = Vue.component('folders-view', {
|
||
data() {
|
||
return {
|
||
componentKey: false, // Flip this value to force re-render the folder accordion thing
|
||
dirName: '' // for the input field
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">Add Folder</div>\
|
||
<form id="choose-directory-form" class="choose-directory-form" @submit.prevent="addFolderDialog">\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="dirName" id="add-directory-name" required type="text" class="validate">\
|
||
<label for="add-directory-name">Display Name</label>\
|
||
<span class="helper-text" >No special characters</span>\
|
||
</div>\
|
||
<button class="btn green waves-effect waves-light select-folder-button" type="submit">\
|
||
Select Folder\
|
||
</button>\
|
||
</form>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="section-header">Your Folders:</div>\
|
||
<template>\
|
||
<folder-accordion :key="componentKey" />\
|
||
</template>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
if (this.dirName === '') {
|
||
document.getElementById("choose-directory-form").reset();
|
||
}
|
||
},
|
||
addFolderDialog: function (event) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedDirectory) => {
|
||
if(selectedDirectory.length === 0 ){
|
||
return;
|
||
}
|
||
|
||
if (loadJson.folders[this.dirName]) {
|
||
iziToast.warning({
|
||
title: 'Display Name Already Exists',
|
||
message: 'Display names must be unique',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
loadJson.folders[this.dirName] = { root: selectedDirectory[0] };
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.componentKey = !this.componentKey;
|
||
this.dirName = '';
|
||
document.getElementById("choose-directory-form").reset();
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const aboutView = Vue.component('about-view', {
|
||
template: '\
|
||
<div>\
|
||
<img class="mstream-logo" src="images/mstream-logo.svg">\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="about-content-section">\
|
||
<div class="about-content-header">Developed By</div>\
|
||
<div class="about-content-body">Paul Sori</div>\
|
||
<div class="about-content-body">paul@mstream.io</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
});
|
||
|
||
var userAccordionHolder;
|
||
Vue.component('user-accordion', {
|
||
data: function() {
|
||
return {
|
||
instances: null,
|
||
users: loadJson.users,
|
||
resetFlag: false,
|
||
closeOverride: () => {
|
||
if (this.resetFlag) {
|
||
this.$parent.componentKey = !this.$parent.componentKey;
|
||
}
|
||
}
|
||
};
|
||
},
|
||
template: '\
|
||
<ul class="z-depth-1 collapsible-folders">\
|
||
<li v-for="(value, key, index) in users">\
|
||
<div v-on:click="toggleOptions(index, $event)" class="collapsible-header">\
|
||
<div class="accordion-header-left">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="100%" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\
|
||
</div>\
|
||
<div class="accordion-header-right">\
|
||
<div><b>{{key}}</b>{{displayName(value)}}{{isGuest(value)}} </div>\
|
||
<div class="user-folders-line">\
|
||
<?xml version="1.0" encoding="utf-8"?><svg width="24" height="22" xmlns="http://www.w3.org/2000/svg" viewBox="6 6 40 40" style="enable-background:new 0 0 48 48"><path d="M16.516 20.688C16.266 21.25 12 31.906 12 31.906V17c0-.55.45-1 1-1h1.334l.35-1.052C14.857 14.427 15.45 14 16 14h5c.55 0 1.143.427 1.316.948l.35 1.052H32c.55 0 1 .45 1 1v3H17.5c-.275 0-.734.125-.984.688zM41 21H19c-.55 0-1.167.418-1.371.929l-5.258 13.143c-.204.51.079.928.629.928h22c.55 0 1.167-.418 1.371-.929l5.258-13.143c.204-.51-.079-.928-.629-.928z"/></svg>\
|
||
{{folderList(value.vpaths)}}\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="collapsible-body clearfix">\
|
||
<a v-if="value\[\'lastfm-user\'\]" v-on:click="removeLastFmUser(value, key, index)" href="#!">Remove last.fm account</a>\
|
||
<a v-else v-on:click="openLastFmModal(value, key, index)" href="#!">Add last.fm account</a>\
|
||
<div class="folder-button-group">\
|
||
<a v-on:click="openUserPasswordModal(value, key, index)" class="waves-effect waves-light btn">Reset Password</a>\
|
||
<a v-on:click="openChangeFoldersModal(value, key, index)" class="waves-effect waves-light btn">Edit</a>\
|
||
<a v-on:click="deleteUser(value, key, index)" class="waves-effect waves-light btn red">\
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path fill="#FFF" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/><path fill="none" d="M0 0h24v24H0z"/></svg>\
|
||
</a>\
|
||
</div>\
|
||
</div>\
|
||
</li>\
|
||
</ul>',
|
||
mounted: function () {
|
||
userAccordionHolder = this;
|
||
this.instances = M.Collapsible.init(document.querySelectorAll('.collapsible-folders'), { onCloseEnd: this.closeOverride });
|
||
},
|
||
beforeDestroy: function() {
|
||
userAccordionHolder = null;
|
||
this.instances[0].destroy();
|
||
},
|
||
methods: {
|
||
displayName: function(arr) {
|
||
if(arr['lastfm-user']) {
|
||
return ' (last.fm: ' + arr['lastfm-user'] + ')';
|
||
}
|
||
return '';
|
||
},
|
||
isGuest: function(arr) {
|
||
if(arr.guest === true) {
|
||
return ' (guest)';
|
||
}
|
||
return '';
|
||
},
|
||
folderList: function(arr) {
|
||
var returnThis = '';
|
||
arr.forEach((element) => {
|
||
returnThis += element + ', '
|
||
});
|
||
returnThis = returnThis.slice(0, -2);
|
||
return returnThis;
|
||
},
|
||
openUserPasswordModal: function(value, key, index) {
|
||
editThisUser = key;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
|
||
vModal.currentViewModal = 'user-password-view';
|
||
modalInstance[0].open();
|
||
},
|
||
openChangeFoldersModal: function(value, key, index) {
|
||
editThisUser = key;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
vModal.currentViewModal = 'user-folders-view';
|
||
modalInstance[0].open();
|
||
},
|
||
deleteUser: function(value, key, index) {
|
||
iziToast.question({
|
||
timeout: 20000,
|
||
close: false,
|
||
overlayClose: true,
|
||
overlay: true,
|
||
displayMode: 'once',
|
||
id: 'question',
|
||
zindex: 99999,
|
||
title: "Delete <b>'" + key + "'</b>?",
|
||
position: 'center',
|
||
buttons: [
|
||
['<button><b>Delete</b></button>', (instance, toast) => {
|
||
delete loadJson.users[key];
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetFlag = true;
|
||
this.instances[0].close(index);
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}, true],
|
||
['<button>Go Back</button>', (instance, toast) => {
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}],
|
||
]
|
||
});
|
||
},
|
||
openLastFmModal: function(value, key, index) {
|
||
editThisUser = key;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
vModal.currentViewModal = 'user-lastfm-view';
|
||
modalInstance[0].open();
|
||
},
|
||
removeLastFmUser: function(value, key, index) {
|
||
iziToast.question({
|
||
timeout: 20000,
|
||
close: false,
|
||
overlayClose: true,
|
||
overlay: true,
|
||
displayMode: 'once',
|
||
id: 'question',
|
||
zindex: 99999,
|
||
title: "Remove last.fm login for <b>'" + key + "'</b>?",
|
||
position: 'center',
|
||
buttons: [
|
||
['<button><b>Delete</b></button>', (instance, toast) => {
|
||
loadJson.users[key]['lastfm-user'] = undefined;
|
||
loadJson.users[key]['lastfm-password'] = undefined;
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
// this.instances[0].close(index);
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
this.$forceUpdate()
|
||
}, true],
|
||
['<button>Go Back</button>', (instance, toast) => {
|
||
instance.hide({ transitionOut: 'fadeOut' }, toast, 'button');
|
||
}],
|
||
]
|
||
});
|
||
},
|
||
toggleOptions: function(index, el) {
|
||
if(el.target.classList.contains('btn')){
|
||
return;
|
||
}
|
||
|
||
if(el.currentTarget.parentElement.classList.contains('active')){
|
||
this.instances[0].close(index);
|
||
return;
|
||
}
|
||
|
||
this.instances[0].open(index);
|
||
}
|
||
}
|
||
});
|
||
|
||
const usersView = Vue.component('users-view', {
|
||
data() {
|
||
return {
|
||
directories: loadJson.folders,
|
||
componentKey: false, // Flip this value to force re-render the folder accordion thing
|
||
newUsername: '', // for the input field
|
||
selectInstance: null,
|
||
newPassword: ''
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">New User</div>\
|
||
<form id="add-user-form" class="" @submit.prevent="addUser">\
|
||
<div class="row row-mod">\
|
||
<div class="input-field directory-name-field col s12 m6">\
|
||
<input @blur="maybeResetForm()" pattern="[a-zA-Z0-9-]+" v-model="newUsername" id="new-username" required type="text" class="validate">\
|
||
<label for="new-username">Username</label>\
|
||
</div>\
|
||
<div class="input-field directory-name-field col s12 m6">\
|
||
<input @blur="maybeResetForm()" v-model="newPassword" id="new-password" required type="password" class="validate">\
|
||
<label for="new-password">Password</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="input-field col s12">\
|
||
<select :disabled="Object.keys(directories).length === 0" id="new-user-dirs" multiple>\
|
||
<option disabled selected value="" v-if="Object.keys(directories).length === 0">You must add a directory before adding a user</option>\
|
||
<option v-for="(key, value) in directories" :value="value">{{ value }}</option>\
|
||
</select>\
|
||
<label for="new-user-dirs">Select User\'s Directories</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12 m6 pad-15">\
|
||
<!-- <a v-on:click="openLastFmModal()" href="#!">Add last.fm account</a> -->\
|
||
<label class="input-field">\
|
||
<input id="is-new-user-guest" type="checkbox"/>\
|
||
<span>Guest Account</span>\
|
||
</label>\
|
||
</div>\
|
||
<div class="col s12 m6">\
|
||
<button id="submit-add-user-form" class="btn green waves-effect waves-light col s6" type="submit">\
|
||
Add user\
|
||
</button>\
|
||
</div>\
|
||
</div>\
|
||
</form>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="section-header">Users:</div>\
|
||
<template>\
|
||
<user-accordion :key="componentKey" />\
|
||
</template>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
mounted: function () {
|
||
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#new-user-dirs"));
|
||
},
|
||
beforeDestroy: function() {
|
||
this.selectInstance[0].destroy();
|
||
},
|
||
methods: {
|
||
openLastFmModal: function() {
|
||
editThisUser = null;
|
||
if (vModal.$children[0]) {
|
||
vModal.componentKey = !vModal.componentKey;
|
||
}
|
||
vModal.currentViewModal = 'user-lastfm-view';
|
||
modalInstance[0].open();
|
||
},
|
||
maybeResetForm: function() {
|
||
if (this.newUsername === '' && this.newPassword === '' && this.selectInstance[0].getSelectedValues().length === 0) {
|
||
document.getElementById("add-user-form").reset();
|
||
}
|
||
},
|
||
addUser: function (event) {
|
||
if (this.selectInstance[0].getSelectedValues().length === 0) {
|
||
iziToast.warning({
|
||
title: 'Cannot add user without a directory',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (loadJson.users[this.newUsername]) {
|
||
iziToast.warning({
|
||
title: 'User Already Exists',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
hashPassword(this.newPassword).then(hashObj => {
|
||
loadJson.users[this.newUsername] = {
|
||
vpaths: this.selectInstance[0].getSelectedValues(),
|
||
password: hashObj.hashPassword,
|
||
salt: hashObj.salt
|
||
};
|
||
|
||
if (document.getElementById("is-new-user-guest").checked === true) {
|
||
loadJson.users[this.newUsername].guest = true;
|
||
}
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.newPassword = '';
|
||
this.newUsername = '';
|
||
this.selectInstance[0].input.value = '';
|
||
this.selectInstance[0].el.value = "";
|
||
this.componentKey = !this.componentKey;
|
||
|
||
document.getElementById("add-user-form").reset();
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const federationView = Vue.component('federation-view', {
|
||
data() {
|
||
return {
|
||
lJson: loadJson,
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">Federation</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
Federation allows you to sync files between mStream servers. To sync your files to another server, you can create a Invite Token through the Web UI\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">Select Federation Folder</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
Federated directories will be downloaded here\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectSyncPath($event)" v-model="lJson.federation.folder" @blur="updateConfig()" type="text" placeholder="Click to select directory" required />\
|
||
<span class="helper-text">Federation is disabled until this value is set</span>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod clearfix">\
|
||
<a class="reset-defaults-link" href="#!" v-on:click="resetStorageValues">Clear (Disables Federation)</a>\
|
||
</div>\
|
||
</div>\
|
||
<div class="card">\
|
||
<div>\
|
||
<div class="section-header">Config Directory</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
Your federation configuration files and security keys are stored here. If you change this directory, you will generate new keys and config files.\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectSyncConfigPath($event)" v-model="lJson.storage.syncConfigDirectory" @blur="updateConfig()" type="text" placeholder="Click to select directory" required />\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod clearfix">\
|
||
<a class="reset-defaults-link" href="#!" v-on:click="resetValue" >Reset To Default Values</a>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
methods: {
|
||
updateConfig: function() {
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
resetStorageValues: function(e) {
|
||
loadJson.federation.folder = '';
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
selectSyncPath: function(el) {
|
||
dialog.showOpenDialog({properties: ['openDirectory']}, (selectedFile) => {
|
||
if(selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile[0];
|
||
loadJson.federation.folder = selectedFile[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectSyncConfigPath: function(el) {
|
||
dialog.showOpenDialog({properties: ['openDirectory']}, (selectedFile) => {
|
||
if (selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile[0];
|
||
loadJson.storage.syncConfigDirectory = selectedFile[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
resetValue: function(e) {
|
||
loadJson.storage.syncConfigDirectory = path.join(app.getPath('userData'), 'sync');
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
}
|
||
}
|
||
});
|
||
|
||
const networkView = Vue.component('network-view', {
|
||
data() {
|
||
return {
|
||
lJson: loadJson,
|
||
selectInstance: null,
|
||
specialTranscodeValue: `${loadJson.transcode.defaultCodec}|${loadJson.transcode.defaultBitrate}`
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">Network Details</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col m6 s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-model="lJson.port" @blur="updateConfig()" id="server-port" type="number" class="validate" min="1" max="65535" step="1" required />\
|
||
<label for="server-port">Port #</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s6">\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">Transcoding</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
Transcoding allows you to listen to your music in a different format. It\'s typically used for low bandwidth streaming on mobile\
|
||
</div>\
|
||
<div class="col s12 transcoding-checkbox">\
|
||
<label class="input-field">\
|
||
<input v-model="lJson.transcode.enabled" v-on:blur="updateConfig" id="allow-transcode-checkbox" type="checkbox" checked="checked" />\
|
||
<span>Enable Transcoding</span>\
|
||
</label>\
|
||
</div>\
|
||
<div class="input-field col s12">\
|
||
<select v-model="specialTranscodeValue" @change="onTranscodeSelectChange($event)" id="transcode-defaults">\
|
||
<option value="mp3|128k">mp3 - 128kbps</option>\
|
||
<option value="mp3|192k">mp3 - 192kbps</option>\
|
||
<option value="opus|64k">opus - 64kbps</option>\
|
||
<option value="opus|96k">opus - 96kbps</option>\
|
||
<option value="opus|128k">opus - 128kbps</option>\
|
||
<option value="aac|64k">aac - 64kbps</option>\
|
||
<option value="aac|96k">aac - 96kbps</option>\
|
||
<option value="aac|128k">aac - 128kbps</option>\
|
||
</select>\
|
||
<label for="transcode-defaults">Select Default Bitrate</label>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">File Uploading</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
You can upload files in the Web App by dropping them into the file explorer\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<label class="input-field">\
|
||
<input v-model="lJson.noUpload" v-on:blur="updateConfig" id="allow-upload-checkbox" type="checkbox" checked="checked" />\
|
||
<span>Disable File Uploading</span>\
|
||
</label>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">Storage</div>\
|
||
</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectDbPath($event)" v-model="lJson.storage.dbDirectory" @blur="updateConfig()" id="server-db-folder" type="text" class="active" required />\
|
||
<label class="active" for="server-db-folder">Database Folder</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectArtPath($event)" v-model="lJson.storage.albumArtDirectory" @blur="updateConfig()" id="server-aa-folder" type="text" required />\
|
||
<label class="active" for="server-aa-folder">Album Art Folder</label>\
|
||
<span class="helper-text">Album art stored here</span>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectLogsPath($event)" v-model="lJson.storage.logsDirectory" @blur="updateConfig()" id="server-logs-folder" type="text" class="active" required />\
|
||
<label class="active" for="server-logs-folder">Logs Folder</label>\
|
||
<span class="helper-text">No logs written if this is empty</span>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s12 port-form-container">\
|
||
<div class="input-field">\
|
||
<input v-on:click="selectFfmpegPath($event)" v-model="lJson.transcode.ffmpegDirectory" @blur="updateConfig()" id="server-ffmpeg-folder" type="text" class="active" required />\
|
||
<label class="active" for="server-ffmpeg-folder">FFmpeg Folder</label>\
|
||
<span class="helper-text">FFmpeg will be downloaded to this directory</span>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row row-mod clearfix">\
|
||
<a class="reset-defaults-link" href="#!" v-on:click="resetStorageValues" >Reset To Default Values</a>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
mounted: function () {
|
||
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#transcode-defaults"));
|
||
},
|
||
beforeDestroy: function() {
|
||
this.selectInstance[0].destroy();
|
||
},
|
||
methods: {
|
||
updateConfig: function() {
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
onTranscodeSelectChange: function(event) {
|
||
const vals = event.target.value.split('|');
|
||
loadJson.transcode.defaultBitrate = vals[1];
|
||
loadJson.transcode.defaultCodec = vals[0];
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
resetStorageValues: function() {
|
||
loadJson.storage.albumArtDirectory = path.join(app.getPath('userData'), 'image-cache');
|
||
loadJson.storage.dbDirectory = path.join(app.getPath('userData'), 'save');
|
||
loadJson.storage.logsDirectory = path.join(app.getPath('userData'), 'logs');
|
||
loadJson.transcode.ffmpegDirectory = path.join(app.getPath('userData'), 'ffmpeg');
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
selectDbPath: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
|
||
if(selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile[0];
|
||
loadJson.storage.dbDirectory = selectedFile[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectFfmpegPath: function(el) {
|
||
dialog.showOpenDialog({ properties: ['openDirectory'] }, (selectedDirectory) => {
|
||
if (selectedDirectory.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedDirectory[0];
|
||
loadJson.transcode.ffmpegDirectory = selectedDirectory[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectLogsPath: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
|
||
if(selectedFile.length === 0 ){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile[0];
|
||
loadJson.storage.logsDirectory = selectedFile[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectArtPath: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openDirectory']}, (selectedFile) => {
|
||
if(selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile[0];
|
||
loadJson.storage.albumArtDirectory = selectedFile[0];
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const securityView = Vue.component('security-view', {
|
||
data: function() {
|
||
return {
|
||
currentViewSsl: (loadJson.ddns.tested) ? ('auto-dns-logged-in-view') : ((!loadJson.ssl.cert && !loadJson.ssl.key) ? 'security-view-signup' : 'security-view-diy')
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="section-header">SSL Encryption</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
This feature is not enabled by default. You must provide your own SSL certificates to enable encryption. SSL certificates can be registered for free with <a href="https://certbot.eff.org/">Certbot</a>\
|
||
<br><br>\
|
||
mStream RPN takes care of registering certificates and configuring server encryption. <a href="https://mstream.io/reverse-proxy-network">Read more about it here</a>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12 security-container">\
|
||
<transition name="component-fade" mode="out-in">\
|
||
<component v-bind:is="currentViewSsl">\
|
||
</component>\
|
||
</transition>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
});
|
||
|
||
const securityViewDiyKeys = Vue.component('security-view-diy', {
|
||
data: function() {
|
||
return {
|
||
ssl: loadJson.ssl
|
||
};
|
||
},
|
||
template: '\
|
||
<div class="card security-diy">\
|
||
<svg v-on:click="clearSSL($event)" class="remove-ssl" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#e53935" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>\
|
||
<div class="section-header">SSL Credentials</div>\
|
||
<div class="row row-mod">\
|
||
<div class="col s12">\
|
||
<div class="input-field">\
|
||
<input v-model="ssl.key" v-on:click="selectKeyFile($event)" id="server-ssl-key" type="text">\
|
||
<label for="server-ssl-key">Key File</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col s12">\
|
||
<div class="input-field">\
|
||
<input v-model="ssl.cert" v-on:click="selectCertFile($event)" id="server-ssl-cert" type="text">\
|
||
<label for="server-ssl-cert">Certificate</label>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
mounted: function() {
|
||
M.updateTextFields();
|
||
},
|
||
methods: {
|
||
clearSSL: function() {
|
||
loadJson.ssl.key = '';
|
||
loadJson.ssl.cert = '';
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
M.updateTextFields();
|
||
this.$parent._data.currentViewSsl = 'security-view-signup';
|
||
},
|
||
selectKeyFile: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openFile']}, (selectedFile) => {
|
||
if(selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile;
|
||
loadJson.ssl.key = selectedFile;
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
selectCertFile: function(el) {
|
||
dialog.showOpenDialog({properties: [ 'openFile']}, (selectedFile) => {
|
||
if(selectedFile.length === 0){
|
||
return;
|
||
}
|
||
el.target.value = selectedFile;
|
||
loadJson.ssl.cert = selectedFile;
|
||
el.target.blur();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
});
|
||
},
|
||
}
|
||
});
|
||
|
||
const securityViewSignup = Vue.component('security-view-signup', {
|
||
data: function() {
|
||
return {
|
||
ssl: loadJson.ssl
|
||
};
|
||
},
|
||
template: '\
|
||
<div class="ssl-choice card no-pad-bot">\
|
||
<div v-on:click="goToAutoDns()" class="signup-button waves-effect waves-default">\
|
||
<p class="center bigger-text"><b>mStream RPN</b></p>\
|
||
<svg height="88" width="100%" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M46.698 33.004H31.99c-3.306 0-5.986 2.68-5.986 5.986v8.295c0 3.637 2.948 6.585 6.585 6.585H46.1c3.637 0 6.585-2.948 6.585-6.585V38.99C52.684 35.684 50.004 33.004 46.698 33.004zM49.547 47.588c0 1.93-1.564 3.494-3.494 3.494H32.635c-1.93 0-3.494-1.564-3.494-3.494v-8.302c0-1.93 1.564-3.494 3.494-3.494h13.419c1.93 0 3.494 1.564 3.494 3.494V47.588zM31.99 31.935h2.01V28.42c0-2.947 2.398-5.345 5.345-5.345 2.947 0 5.345 2.398 5.345 5.345v3.515h2.01c.574 0 1.133.071 1.668.201V28.42c0-4.975-4.047-9.022-9.022-9.022s-9.022 4.047-9.022 9.022v3.716C30.857 32.006 31.415 31.935 31.99 31.935z" fill="#3E4753"/><path d="M45.095,37.451H33.593c-1.287,0-2.331,1.043-2.331,2.331v7.311c0,1.287,1.043,2.331,2.331,2.331h11.503 c1.287,0,2.331-1.043,2.331-2.331v-7.311C47.426,38.494,46.382,37.451,45.095,37.451z M40.705,43.86v2.064 c0,0.748-0.612,1.361-1.361,1.361c-0.748,0-1.361-0.612-1.361-1.361V43.86c-0.599-0.426-0.991-1.126-0.991-1.918 c0-1.299,1.053-2.353,2.353-2.353c1.299,0,2.353,1.054,2.353,2.353C41.697,42.734,41.305,43.434,40.705,43.86z" fill="#2E7EB8"/><g><path d="M60.7,29.153c-1.119-1.406-2.581-2.503-4.224-3.19c-0.306-4.111-2.037-7.917-4.952-10.832 c-3.225-3.225-7.542-5.002-12.154-5.002c-5.136,0-9.986,2.323-13.291,6.292c-1.267-0.358-2.555-0.539-3.848-0.539 c-6.201,0-11.544,3.92-13.466,9.701C6.783,26.166,5,27.308,3.638,28.889C1.937,30.865,1,33.402,1,36.035 c0,5.957,4.846,10.803,10.803,10.803h2.839h8.154V42.25h-8.154h-2.839c-3.427,0-6.215-2.788-6.215-6.215 c0-3.173,2.246-5.796,5.341-6.238l1.571-0.224l0.344-1.549c0.988-4.446,4.848-7.552,9.386-7.552c1.27,0,2.546,0.263,3.792,0.782 l1.731,0.721l1.051-1.554c2.416-3.571,6.365-5.703,10.564-5.703c7.047,0,12.567,5.52,12.567,12.567v2.193l1.789,0.404 c2.842,0.642,4.768,3.145,4.684,6.086l-0.001,0.066c0,2.041-0.993,3.85-2.517,4.984v5.154c4.128-1.511,7.091-5.462,7.105-10.105 C63.061,33.55,62.246,31.095,60.7,29.153z" fill="#2E7EB8"/></g></svg>\
|
||
<p class="signup-price">Sign Up / Login</p>\
|
||
</div>\
|
||
<div v-on:click="goToDiy()" class="diy-button clearfix waves-effect waves-default">\
|
||
<svg class="ssl-svg3 center" height="60" width="100%" viewBox="0 0 33 33" xmlns="http://www.w3.org/2000/svg"><path d="M13.641 8.239c2.021-1.023 3.667-2.107 4.591-2.76 1.732 1.223 5.988 3.967 10.535 4.972-.121 1.343-.432 3.75-1.211 6.384l2.445 1.14c1.351-4.386 1.468-8.24 1.476-8.577l.026-1.163-1.152-.17C24.813 7.249 19.1 2.848 19.043 2.805l-.81-.631-.81.63C17.387 2.833 15 4.67 11.825 6.192 12.553 6.762 13.167 7.456 13.641 8.239zM18.232 28.08c-4.332-1.375-6.896-4.98-8.415-8.664-.851.328-1.754.504-2.677.514 1.758 4.531 4.947 9.158 10.734 10.795l.357.102.359-.102c3.113-.881 5.475-2.629 7.267-4.76l-2.523-1.178C21.988 26.24 20.316 27.418 18.232 28.08z" fill="#2B79C2"/><path d="M26.357 16.275c.527-1.839.818-3.558.979-4.827-3.756-1.059-7.178-3.089-9.104-4.373-.992.662-2.383 1.521-4.011 2.344.172.443.307.904.396 1.382L26.357 16.275zM18.232 26.688c1.516-.551 2.782-1.422 3.845-2.486l-11.01-5.135C12.42 22.287 14.627 25.379 18.232 26.688z" fill="#2B79C2"/><path d="M30.555,19.479l-16.966-7.912c-0.23-2.219-1.58-4.266-3.749-5.276C7.375,5.141,4.561,5.655,2.662,7.37 L7.616,9.68l0.639,3.115l-2.797,1.512l-4.953-2.311c-0.094,2.557,1.322,5.042,3.787,6.193c2.168,1.01,4.604,0.729,6.451-0.52 l16.966,7.91c1.686,0.787,3.688,0.057,4.475-1.627C32.969,22.268,32.24,20.264,30.555,19.479z M28.001,23.615 c-0.731-0.34-1.048-1.211-0.707-1.943s1.212-1.049,1.943-0.707s1.049,1.211,0.708,1.943C29.604,23.641,28.733,23.957,28.001,23.615 z" fill="#454C51"/></svg>\
|
||
<div><p class="center bigger-text"><b>DIY</b></p></div>\
|
||
</div>\
|
||
</div>',
|
||
methods: {
|
||
goToDiy: function() {
|
||
this.$parent._data.currentViewSsl = 'security-view-diy';
|
||
},
|
||
goToAutoDns: function() {
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'auto-dns-view';
|
||
resetNav(document.getElementById("nav-auto-dns"));
|
||
}
|
||
}
|
||
});
|
||
|
||
const userPasswordView = Vue.component('user-password-view', {
|
||
data() {
|
||
return {
|
||
users: loadJson.users,
|
||
currentUser: editThisUser,
|
||
resetPassword: '',
|
||
componentKey: false,
|
||
};
|
||
},
|
||
template: '\
|
||
<form @submit.prevent="updatePassword" id="reset-password-form">\
|
||
<div class="modal-content">\
|
||
<h4>Password Reset </h4>\
|
||
<p>User: <b>{{currentUser}}</b></p>\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" v-model="resetPassword" id="reset-password" required type="password" class="validate">\
|
||
<label for="reset-password">New Password</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="modal-footer">\
|
||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
|
||
<button id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
|
||
Update Password\
|
||
</button>\
|
||
</div>\
|
||
</form>',
|
||
methods: {
|
||
maybeResetForm: function() {
|
||
if (this.resetPassword === '') {
|
||
document.getElementById("reset-password-form").reset();
|
||
}
|
||
},
|
||
updatePassword: function() {
|
||
hashPassword(this.resetPassword).then(hashObj => {
|
||
loadJson.users[this.currentUser].password = hashObj.hashPassword;
|
||
loadJson.users[this.currentUser].salt = hashObj.salt;
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
this.resetPassword = '';
|
||
|
||
document.getElementById("reset-password-form").reset();
|
||
modalInstance[0].close();
|
||
|
||
iziToast.success({
|
||
title: 'Password Updated',
|
||
position: 'topCenter',
|
||
timeout: 2500
|
||
});
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
const userFoldersView = Vue.component('user-folders-view', {
|
||
data() {
|
||
return {
|
||
directories: loadJson.folders,
|
||
users: loadJson.users,
|
||
currentUser: editThisUser,
|
||
selectInstance: null,
|
||
};
|
||
},
|
||
template: '\
|
||
<form @submit.prevent="updateFolders" id="update-folders-form">\
|
||
<div class="modal-content">\
|
||
<h4>Edit User</h4>\
|
||
<p>User: <b>{{currentUser}}</b></p>\
|
||
<label for="edit-user-dirs">Select User\'s Directories</label>\
|
||
<select :disabled="Object.keys(directories).length === 0" id="edit-user-dirs" multiple>\
|
||
<option disabled selected value="" v-if="Object.keys(directories).length === 0">You must add a directory before adding a user</option>\
|
||
<option :selected="users[currentUser].vpaths.includes(value)" v-for="(key, value) in directories" :value="value">{{ value }}</option>\
|
||
</select>\
|
||
<div class="edit-user-guest">\
|
||
<label class="input-field">\
|
||
<input v-model="users[currentUser].guest" v-on:blur="updateConfig" type="checkbox"/>\
|
||
<span>Guest Account</span>\
|
||
</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="modal-footer">\
|
||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
|
||
<button id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
|
||
Update\
|
||
</button>\
|
||
</div>\
|
||
</form>',
|
||
mounted: function () {
|
||
this.selectInstance = M.FormSelect.init(document.querySelectorAll("#edit-user-dirs"));
|
||
},
|
||
beforeDestroy: function() {
|
||
this.selectInstance[0].destroy();
|
||
},
|
||
methods: {
|
||
updateConfig: function() {
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
},
|
||
updateFolders: function() {
|
||
loadJson.users[this.currentUser].vpaths = this.selectInstance[0].getSelectedValues();
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
modalInstance[0].close();
|
||
|
||
iziToast.success({
|
||
title: 'User Updated',
|
||
position: 'topCenter',
|
||
timeout: 2500
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
const userLastFmView = Vue.component('user-lastfm-view', {
|
||
data() {
|
||
return {
|
||
currentUser: editThisUser,
|
||
users: loadJson.users,
|
||
lastFmUser: '',
|
||
lastFmPassword: '',
|
||
lastFmVerified: false,
|
||
pending: false
|
||
};
|
||
},
|
||
template: '\
|
||
<form @submit.prevent="addLastFmAccount" id="add-lastfm-user-form">\
|
||
<div class="modal-content">\
|
||
<h4>Add last.fm Account</h4>\
|
||
<p v-if="currentUser">User: <b>{{currentUser}}</b></p>\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" v-model="lastFmUser" id="lastfm-username" required type="text" class="validate">\
|
||
<label for="lastfm-username">last.fm user</label>\
|
||
</div>\
|
||
<div class="input-field directory-name-field">\
|
||
<input @blur="maybeResetForm()" v-model="lastFmPassword" id="lastfm-password" required type="password" class="validate">\
|
||
<label for="lastfm-password">last.fm password</label>\
|
||
</div>\
|
||
</div>\
|
||
<div class="modal-footer">\
|
||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Go Back</a>\
|
||
<button :disabled="pending" id="submit-reset-password-form" class="btn green waves-effect waves-light" type="submit">\
|
||
{{pending === false ? "LOGIN": "Pending..."}}\
|
||
</button>\
|
||
</div>\
|
||
</form>',
|
||
methods: {
|
||
addLastFmAccount: async function() {
|
||
this.pending = true;
|
||
// validate last.fm credentials
|
||
if(await testLastFm(this.lastFmUser, this.lastFmPassword) === false) {
|
||
this.pending = false;
|
||
iziToast.warning({
|
||
title: 'Failed to Login to Last.fm',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.pending = false;
|
||
|
||
if (this.currentUser) {
|
||
loadJson.users[this.currentUser]['lastfm-user'] = this.lastFmUser;
|
||
loadJson.users[this.currentUser]['lastfm-password'] = this.lastFmPassword;
|
||
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
modalInstance[0].close();
|
||
|
||
// Force update component if it exists
|
||
if(userAccordionHolder) {
|
||
userAccordionHolder.$forceUpdate();
|
||
}
|
||
return;
|
||
}
|
||
},
|
||
maybeResetForm: function() {
|
||
if (this.lastFmUser === '' && this.lastFmPassword === '') {
|
||
document.getElementById("add-lastfm-user-form").reset();
|
||
}
|
||
},
|
||
}
|
||
});
|
||
|
||
async function testLastFm(username, password) {
|
||
const apiKey1 = '25627de528b6603d6471cd331ac819e0';
|
||
const apiKey2 = 'a9df934fc504174d4cb68853d9feb143';
|
||
|
||
const token = crypto.createHash('md5').update(username + crypto.createHash('md5').update(password, 'utf8').digest("hex"), 'utf8').digest("hex");
|
||
const cryptoString = `api_key${apiKey1}authToken${token}methodauth.getMobileSessionusername${username}${apiKey2}`;
|
||
const hash = crypto.createHash('md5').update(cryptoString, 'utf8').digest("hex");
|
||
|
||
// Try logging in
|
||
try {
|
||
await axios({
|
||
method: 'get',
|
||
url: `http://ws.audioscrobbler.com/2.0/?method=auth.getMobileSession&username=${username}&authToken=${token}&api_key=${apiKey1}&api_sig=${hash}`
|
||
});
|
||
}catch (err) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
const bootView = Vue.component('boot-view', {
|
||
template: "\
|
||
<div>\
|
||
<div id='bars'>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
<div class='bar'></div>\
|
||
</div>\
|
||
<div id='boot-server-text' class='boot-server-text'>\
|
||
Booting Server\
|
||
</div>\
|
||
<div id='post-boot-content'></div>\
|
||
</div>",
|
||
});
|
||
|
||
const autoDnsView = Vue.component('auto-dns-view', {
|
||
data() {
|
||
return {
|
||
pending: false,
|
||
currentViewDdnsLogin: 'ddns-view-login',
|
||
username: '',
|
||
password: ''
|
||
};
|
||
},
|
||
template: '\
|
||
<div>\
|
||
<div class="row auto-dns-selection">\
|
||
<div class="col m6 s12 auto-dns-selection-item">\
|
||
<div v-on:click="goToSignUp()" class="card auto-dns-card auto-dns-sign-up-item">\
|
||
<div class="section-header">Sign Up</div>\
|
||
<svg viewBox="0 0 128 128" height="120" width="100%" xmlns="http://www.w3.org/2000/svg"><path d="M94.894,68.493c-1.379-4.803-5.663-8.043-10.661-8.063h-0.631c0.146-3.979-0.709-7.176-2.677-10.032 c-2.403-3.498-5.605-5.677-9.788-6.663c-1.014-0.237-2.09-0.357-3.197-0.357c-4.143,0-9.78,1.83-13.333,6.942 c-1.344-0.545-2.772-0.822-4.249-0.822c-5.846,0-10.765,4.5-11.299,10.287c-0.633,0.333-1.228,0.719-1.773,1.149 c-1.67,1.311-2.967,3.049-3.752,5.025c-1.284,3.234-1.095,6.848,0.518,9.914c1.72,3.263,4.408,5.412,7.774,6.215l0.023-0.096 c0.691,0.135,2.776,0.148,9.075,0.188l5.487,0.037l0.012,0.071l0.348,0.003c3.083,0.029,9.591,0.055,15.166,0.076l4.354,0.018 l-0.024,0.042l6.333,0.045c0.85,0,1.507-0.01,2.126-0.029c3.362-0.11,6.442-1.754,8.451-4.513 C95.189,75.181,95.816,71.743,94.894,68.493z M43.606,62.33c-0.108-0.479-0.162-0.98-0.162-1.494c0-3.807,3.102-6.904,6.915-6.904 c1.844,0,3.579,0.724,4.884,2.039l0.184,0.186l0.247-0.087c0.697-0.244,1.267-0.758,1.565-1.407 c0.272-0.583,0.566-1.105,0.901-1.598c2.562-3.852,6.742-5.224,9.8-5.224c0.748,0,1.483,0.08,2.181,0.239 c3.054,0.717,5.386,2.302,7.132,4.846c1.572,2.293,2.148,5.027,1.813,8.601l-0.039,0.401c-0.078,0.745,0.174,1.503,0.693,2.082 c0.514,0.561,1.241,0.884,1.998,0.884h2.496c3.005,0.01,5.571,1.949,6.385,4.831c0.55,1.897,0.177,3.933-1.022,5.587 c-1.191,1.632-3.01,2.611-4.991,2.682c-0.573,0.019-1.188-0.026-1.983,0.019H45.094c-3.293-0.01-5.615-1.387-7.097-4.216 c-1.002-1.913-1.118-4.172-0.32-6.194c0.504-1.246,1.32-2.342,2.346-3.154l0.12-0.089c0.522-0.389,1.062-0.703,1.594-0.927 c0.036-0.013,0.109-0.031,0.261-0.073c0.359-0.102,0.902-0.256,1.423-0.587l0.247-0.156L43.606,62.33z" fill="#2E79BE"/><path d="M116.277,93.951c0.098-0.469,0.137-0.958,0.137-1.447v-59.98c0-5.357-4.047-9.677-9.032-9.677h-1.916 h-4.086H20.638c-4.985,0-9.032,4.321-9.032,9.677v59.98c0,0.489,0.039,0.977,0.098,1.447H2.535v1.505 c0,5.376,4.027,9.697,9.012,9.697h104.906c4.966,0,9.013-4.321,9.013-9.697v-1.505H116.277z M71.194,100.246 c0,1.036-0.684,1.877-1.564,1.877H58.37c-0.88,0-1.564-0.821-1.564-1.877v-1.212c0-1.056,0.684-1.896,1.564-1.896H69.63 c0.88,0,1.564,0.86,1.564,1.896V100.246z M107.577,93.951H20.423V32.368h79.093h3.882h4.179V93.951z" fill="#2D3E4F"/></svg>\
|
||
</div>\
|
||
</div>\
|
||
<div class="col m6 s12 auto-dns-selection-item">\
|
||
<div class="card auto-dns-card">\
|
||
<div class="section-header">Login</div>\
|
||
<form id="login-form" class="" @submit.prevent="login">\
|
||
<div class="row row-mod">\
|
||
<div class="input-field directory-name-field col s12">\
|
||
<input @blur="maybeResetForm()" v-model="username" id="login-username" required type="text" class="validate">\
|
||
<label for="login-username">Username</label>\
|
||
</div>\
|
||
<div class="input-field directory-name-field col s12">\
|
||
<input @blur="maybeResetForm()" v-model="password" id="login-password" required type="password" class="validate">\
|
||
<label for="login-password">Password</label>\
|
||
</div>\
|
||
<div class="col s12">\
|
||
<button :disabled="pending" class="btn green waves-effect waves-light login-button" type="submit">\
|
||
{{loginButtonText}}\
|
||
</button>\
|
||
</div>\
|
||
</div>\
|
||
</form>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">mStream RPN Service</div>\
|
||
<div class="col s12">\
|
||
<ul class="browser-default">\
|
||
<li>Get your own personal domain @ https://your-name.mstream.io</li>\
|
||
<li>Automatically configures SSL Encryption for your server</li>\
|
||
<li>State of the art \'Hole Punching\' software guarantees your server stays online as long as you have a working internet connection</li>\
|
||
<li>IP Obfuscation hides your IP address and adds an additional layer of security</li>\
|
||
</ul>\
|
||
mStream RPN Service comes with everything you need to get your new server online in seconds. Think of it as your private cloud.\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
computed: {
|
||
loginButtonText: function() {
|
||
if(this.pending) {
|
||
return 'pending...';
|
||
}
|
||
return 'Login';
|
||
}
|
||
},
|
||
methods: {
|
||
goToSignUp: function() {
|
||
shell.openExternal('https://mstream.io/reverse-proxy-network');
|
||
},
|
||
maybeResetForm: function() {
|
||
if (this.username === '' && this.password === '') {
|
||
document.getElementById("login-form").reset();
|
||
}
|
||
},
|
||
login: async function() {
|
||
try {
|
||
this.pending = true;
|
||
const loginRes = await axios({
|
||
method: 'post',
|
||
url: apiEndpoint + '/login',
|
||
headers: { 'accept': 'application/json' },
|
||
responseType: 'json',
|
||
data: {
|
||
email: this.username,
|
||
password: this.password
|
||
}
|
||
});
|
||
const configRes = await axios({
|
||
method: 'get',
|
||
url: apiEndpoint + '/account/info',
|
||
headers: { 'x-access-token': loginRes.data.token, 'accept': 'application/json' },
|
||
responseType: 'json'
|
||
});
|
||
loadJson.ddns = {
|
||
email: this.username,
|
||
password: this.password,
|
||
token: loginRes.data.token,
|
||
tested: true,
|
||
url: configRes.data.subdomain + '.' + configRes.data.domain
|
||
};
|
||
fs.writeFileSync(configFile, JSON.stringify(loadJson), 'utf8');
|
||
iziToast.success({
|
||
title: 'Logged In',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
vm.currentViewMain = 'auto-dns-logged-in-view';
|
||
this.pending = false;
|
||
} catch(err) {
|
||
this.pending = false;
|
||
iziToast.warning({
|
||
title: 'Failed to Login',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const autoDnsLoggedInView = Vue.component('auto-dns-logged-in-view', {
|
||
template: '\
|
||
<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="row row-mod">\
|
||
<div class="section-header">mStream RPN</div>\
|
||
<div class="col s12">\
|
||
<div>You are logged in as: <b>{{loadJson.ddns.email}}</b></div>\
|
||
<div>Your Public URL: <b>https://{{loadJson.ddns.url}}</b></div>\
|
||
<br>\
|
||
<div>SSL Encryption: <b>Enabled</b></div>\
|
||
<br>\
|
||
<div v-if="this.$parent && this.$parent.$el && this.$parent.$el.id === \'switcherMain\'"><a v-on:click="logout()" href="javascript:;">Logout</a></div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>',
|
||
methods: {
|
||
logout: function() {
|
||
loadJson.ddns = {};
|
||
vm.currentViewMain = 'auto-dns-view';
|
||
},
|
||
}
|
||
});
|
||
|
||
var vModal = new Vue({
|
||
el: '#switcherModal',
|
||
components: {
|
||
'user-password-view': userPasswordView,
|
||
'user-folders-view': userFoldersView,
|
||
'user-lastfm-view': userLastFmView
|
||
},
|
||
data: {
|
||
componentKey: false,
|
||
currentViewModal: false
|
||
}
|
||
});
|
||
|
||
var vm = new Vue({
|
||
el: '#switcherMain',
|
||
components: {
|
||
'folders-view': foldersView,
|
||
'about-view': aboutView,
|
||
'users-view': usersView,
|
||
'network-view': networkView,
|
||
'federation-view': federationView,
|
||
'security-view': securityView,
|
||
'boot-view': bootView,
|
||
'auto-dns-view': autoDnsView
|
||
},
|
||
data: {
|
||
currentViewMain: false
|
||
}
|
||
});
|
||
|
||
vm.currentViewMain = 'folders-view';
|
||
|
||
document.getElementById("nav-directories").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'folders-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-about").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'about-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-users").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'users-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-network").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'network-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-security").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'security-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-auto-dns").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = (loadJson.ddns.tested) ? 'auto-dns-logged-in-view' : 'auto-dns-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
document.getElementById("nav-federation").onclick = function(){
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
vm.currentViewMain = 'federation-view';
|
||
resetNav(this);
|
||
}
|
||
|
||
function resetNav(that) {
|
||
document.querySelectorAll('.left-nav-button').forEach((el) => {
|
||
el.classList.remove("nav-selected");
|
||
});
|
||
|
||
if (that) {
|
||
that.classList.add('nav-selected');
|
||
}
|
||
}
|
||
|
||
document.getElementById("boot-server-button").onclick = function(){
|
||
if(!loadJson.folders) {
|
||
iziToast.warning({
|
||
title: 'You must set tat least one folder',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
resetNav(document.getElementById("nav-directories"));
|
||
vm.currentViewMain = 'folders-view';
|
||
return;
|
||
}
|
||
|
||
if(!loadJson.port) {
|
||
iziToast.warning({
|
||
title: 'You must set the port before booting the server',
|
||
position: 'topCenter',
|
||
timeout: 3500
|
||
});
|
||
resetNav(document.getElementById("nav-network"));
|
||
vm.currentViewMain = 'network-view';
|
||
return;
|
||
}
|
||
|
||
if(bootFlag === true) {
|
||
return;
|
||
}
|
||
|
||
vm.currentViewMain = 'boot-view';
|
||
document.getElementById("boot-server-button").innerHTML = 'Booting...'
|
||
bootFlag = true;
|
||
resetNav();
|
||
|
||
if (document.getElementById("boot-server-checkbox").checked === true) {
|
||
loadJson.autoboot = true;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
ipcRenderer.send('start-server', loadJson);
|
||
setTimeout(() => {
|
||
document.getElementById("boot-server-button").innerHTML = 'Success';
|
||
document.getElementById("boot-server-text").innerHTML = 'Server Booted!';
|
||
|
||
// document.getElementById("post-boot-content").innerHTML = '<div>This window will automatically close in <span id="countdown">30</span> seconds to save on memory</div>';
|
||
const localhostAdd = loadJson.ssl && loadJson.ssl.cert && loadJson.ssl.key ? 'https' : 'http' + '://localhost:' + loadJson.port;
|
||
document.getElementById("post-boot-content").innerHTML = '<div class="row">\
|
||
<div class="col s12">\
|
||
<div class="card">\
|
||
<div class="about-content-section">\
|
||
<div>This window will automatically close in <span id="countdown">30</span> seconds to save on memory</div>\
|
||
<br>\
|
||
<div>Test mStream locally at: <a href="'+localhostAdd+'">'+localhostAdd+'</a></div>\
|
||
</div>\
|
||
</div>\
|
||
</div>\
|
||
</div>';
|
||
|
||
var countdown = 30;
|
||
setInterval(() => {
|
||
countdown--;
|
||
if(countdown === 0){
|
||
var window = remote.getCurrentWindow();
|
||
window.close();
|
||
}else{
|
||
document.getElementById("countdown").innerHTML = countdown;
|
||
}
|
||
}, 1000);
|
||
}, 2500);
|
||
}, 2500);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |