Refactor web (#26)

This commit was merged in pull request #26.
This commit is contained in:
2023-11-20 07:38:16 +10:30
committed by GitHub
parent 6e2c8d17a1
commit adb9922b52
10 changed files with 500 additions and 411 deletions

5
web/data/js/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,204 @@
{{ define "content" }}
{{ template "menu.tmpl" . }}
<div x-data="config()" x-init="fetch_config();">
<p class="error" x-show="error_message" x-transition.duration.500ms x-text="error_message"></p>
<p class="success" x-show="success_message" x-transition.duration.500ms x-text="success_message"></p>
<p>Note: changes are not saved until the "Save Config" button is pressed.</p>
<div class="pure-g">
<div class="pure-u-1">
<button class="pure-button pure-button-primary" @click="save_config();" href="#">Save Config</button>
</div>
</div>
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1 l-box">
<form class="pure-form pure-form-stacked gropple-config">
<fieldset>
<legend>Server</legend>
<label for="config-server-port">Listen Port</label>
<input type="text" id="config-server-port" placeholder="port number" x-model.number="config.server.port" />
<span class="pure-form-message">The port the web server will listen on.</span>
<label for="config-server-address">Server address (URL)</label>
<input type="text" id="config-server-address" class="input-long" placeholder="server address" x-model="config.server.address" />
<span class="pure-form-message">
The address the service will be available on. Generally it will be http://hostname:port where
hostname is the host the server is running on, and port is the port you set above.
</span>
<label for="config-server-downloadpath">Download path</label>
<input type="text" id="config-server-downloadpath" placeholder="path" class="input-long" x-model="config.server.download_path" />
<span class="pure-form-message">The default path on the server to download files to.</span>
<label for="config-server-max-downloads">Maximum active downloads per domain</label>
<input type="text" id="config-server-max-downloads" placeholder="2" class="input-long" x-model.number="config.server.maximum_active_downloads_per_domain" />
<span class="pure-form-message">How many downloads can be simultaneously active. Use '0' for no limit. This limit is applied per domain that you download from.</span>
<legend>UI</legend>
<p>Note that changes to the popup dimensions will require you to recreate your bookmarklet.</p>
<label for="config-ui-popupwidth">Popup Width</label>
<input type="text" id="config-ui-popupwidth" placeholder="width in pixels" x-model.number="config.ui.popup_width" />
<span class="pure-form-message">The width of popup windows in pixels.</span>
<label for="config-ui-popupheight">Popup Height</label>
<input type="text" id="config-ui-popupheight" placeholder="height in pixels" x-model.number="config.ui.popup_height" />
<span class="pure-form-message">The height of popup windows in pixels.</span>
</fieldset>
</form>
</div>
<div class="pure-u-lg-1-3 pure-u-1 l-box">
<form class="pure-form gropple-config">
<fieldset>
<legend>Download Profiles</legend>
<p>Gropple supports multiple download profiles. Each profile specifies a different youtube-dl
compatible command, and arguments. When starting a download, you may choose which profile
to use. The URL will be appended to the argument list at the end.
</p>
<hr>
<template x-for="(profile, i) in config.profiles">
<div>
<label x-bind:for="'config-profiles-'+i+'-name'">Name of profile <span x-text="i+1"></span>
</label>
<input type="text" x-bind:id="'config-profiles-'+i+'-name'" class="input-long" placeholder="name" x-model="profile.name" />
<button class="pure-button button-del" href="#" @click.prevent="config.profiles.splice(i, 1);;">delete profile</button>
<span class="pure-form-message">The name of this profile. For your information only.</span>
<label x-bind:for="'config-profiles-'+i+'-command'">Command to run</label>
<input type="text" x-bind:id="'config-profiles-'+i+'-command'" class="input-long" placeholder="name" x-model="profile.command" />
<span class="pure-form-message">Which command to run. Your path will be searched, or you can specify the full path here.</span>
<label>Arguments</label>
<template x-for="(arg, j) in profile.args">
<div>
<input type="text" x-bind:id="'config-profiles-'+i+'-arg-'+j" placeholder="arg" x-model="profile.args[j]" />
<button class="pure-button button-del" href="#" @click.prevent="profile.args.splice(j, 1);;">delete arg</button>
</div>
</template>
<button class="pure-button button-add" href="#" @click.prevent="profile.args.push('');">add arg</button>
<span class="pure-form-message">Arguments for the command. Note that the shell is not used, so there is no need to quote or escape arguments, including those with spaces.</span>
<hr>
</div>
</template>
<button class="pure-button button-add" href="#" @click.prevent="config.profiles.push({name: 'new profile', command: 'youtube-dl', args: []});">add profile</button>
</fieldset>
</form>
</div>
<div class="pure-u-lg-1-3 pure-u-1 l-box">
<form class="pure-form gropple-config">
<fieldset>
<legend>Destinations</legend>
<p>You can specify custom destinations (directories) here. Downloads can be
moved to one of these directories after completion from the index page,
if you do not want them to be left in the download path above.</p>
</p>
<template x-for="(dest, i) in config.destinations">
<div>
<label x-bind:for="'config-destinations-'+i+'-name'">Name of destination <span x-text="i+1"></span>
</label>
<input type="text" x-bind:id="'config-destinations-'+i+'-name'" class="input-long" placeholder="name" x-model="dest.name" />
<span class="pure-form-message">The name of this destination. For your information only.</span>
<label x-bind:for="'config-destinations-'+i+'-command'">Path</label>
<input type="text" x-bind:id="'config-destinations-'+i+'-command'" class="input-long" placeholder="name" x-model="dest.path" />
<span class="pure-form-message">Path to move completed downloads to.</span>
<button class="pure-button button-del" href="#" @click.prevent="config.destinations.splice(i, 1);">delete destination</button>
<hr>
</div>
</template>
<button class="pure-button button-add" href="#" @click.prevent="config.destinations.push({name: 'new destination', path: '/tmp'});">add destination</button>
</fieldset>
</form>
</div>
<div class="pure-g">
<div class="pure-u-1">
<button class="pure-button pure-button-primary" @click="save_config();" href="#">Save Config</button>
</div>
</div>
</div>
{{ end }}
{{ define "js" }}
<script>
function config() {
return {
config: { server : {}, ui : {}, profiles: [], destinations: []},
error_message: '',
success_message: '',
fetch_config() {
fetch('/rest/config')
.then(response => response.json())
.then(config => {
this.config = config;
})
.catch(error => {
console.log('failed to fetch config', error);
});
},
save_config() {
let op = {
method: 'POST',
body: JSON.stringify(this.config),
headers: { 'Content-Type': 'application/json' }
};
fetch('/rest/config', op)
.then(response => {
return response.json();
})
.then(response => {
if (response.error) {
this.error_message = response.error;
this.success_message = '';
document.body.scrollTop = document.documentElement.scrollTop = 0;
} else {
this.error_message = '';
this.success_message = 'configuration saved';
document.body.scrollTop = document.documentElement.scrollTop = 0;
this.config = response;
}
})
.catch(error => {
console.log('exception' ,error);
});
}
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,98 @@
{{ define "content" }}
{{ template "menu.tmpl" . }}
<div x-data="index()" x-init="fetch_data(); fetch_version()">
<p x-cloak x-show="version && version.upgrade_available">
<a href="https://github.com/tardisx/gropple/releases">Upgrade is available</a> -
you have
<span x-text="version.current_version"></span> and
<span x-text="version.github_version"></span>
is available.</p>
<div>
<p>
Drag this bookmarklet: <a href="{{ .BookmarkletURL }}">Gropple</a> to your bookmark bar, and click it
on any page you want to grab the video from.
</p>
<p>
Please note that some adblockers may prevent the bookmarklet from opening the popup window.
</p>
</div>
<table class="pure-table">
<thead>
<tr>
<th>id</th><th>filename</th><th>url</th><th>show</th><th>state</th><th>percent</th><th>eta</th><th>finished</th>
</tr>
</thead>
<tbody>
<template x-for="item in items">
<tr>
<td x-text="item.id"></td>
<td>
<span x-show="item.files && item.files.length > 0">
<ul>
<template x-for="file in item.files">
<li x-text="file"></li>
</template>
</ul>
</span>
<span x-show="! item.files || item.files.length == 0"
x-text="item.url">
</span>
</td>
<td><a class="int-link" x-bind:href="item.url">&#x2197;</a></td>
<td><a class="int-link" @click="show_popup(item)" href="#">&#x1F4C4;</a></td>
<td :class="'state-'+item.state" x-text="item.state"></td>
<td x-text="item.percent"></td>
<td x-text="item.eta"></td>
<td x-text="item.finished ? '&#x2714;' : '-'"></td>
</tr>
</template>
</tbody>
</table>
</div>
{{ end }}
{{ define "js" }}
<script>
function index() {
return {
items: [], version: {}, popups: {},
fetch_version() {
fetch('/rest/version')
.then(response => response.json())
.then(info => {
this.version = info;
setTimeout(() => { this.fetch_version() }, 1000 * 60 );
})
.catch(error => {
console.log('failed to fetch version info - will retry');
setTimeout(() => { this.fetch_version() }, 1000 );
});
},
fetch_data() {
fetch('/rest/fetch')
.then(response => response.json())
.then(info => {
// will be null if no downloads yet
if (info) {
this.items = info;
}
setTimeout(() => { this.fetch_data() }, 1000);
})
},
show_popup(item) {
// allegedly you can use the reference to pop the window to the front on subsequent
// clicks, but I can't seem to find a reliable way to do so.
this.popups[item.id] = window.open(item.popup_url, item.id, "width={{ .Config.UI.PopupWidth }},height={{ .Config.UI.PopupHeight }}");
},
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,83 @@
{{ define "layout" }}
<html lang="en">
<head>
<meta charset="utf-8">
<title>gropple</title>
<script src="/static/alpine.min.js" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css">
<style>
.pure-g > div {
box-sizing: border-box;
}
.l-box {
padding: 2em;
}
pre {
font-size: 60%;
height: 100px;
overflow:auto;
}
footer {
padding-top: 50px;
font-size: 30%;
}
.int-link {
text-decoration: none;
hover { color: red; }
}
.state-failed {
color: red;
}
.state-downloading {
color: blue;
}
.state-moved {
color: green;
}
.state-complete {
color: green;
}
.gropple-config {
font-size: 80%;
}
.gropple-config input.input-long {
width: 27em;
}
.gropple-config button {
border-radius: 12px;
}
.gropple-config button.button-del {
background: rgb(202, 60, 60);
}
.gropple-config button.button-add {
background: rgb(60, 200, 60);
}
.gropple-config .pure-form-message {
padding-top: .5em;
padding-bottom: 1.5em;
}
.error {
color: red;
font-size: 150%;
}
.success {
color: green;
}
[x-cloak] { display: none !important; }
</style>
</head>
<body style="margin:4; padding:4">
{{ template "content" . }}
<footer>
Homepage: <a href="https://github.com/tardisx/gropple">https://github.com/tardisx/gropple</a><br>
Version: {{ .Version.CurrentVersion }}
</footer>
</body>
{{ template "js" . }}
</html>
{{ end }}

View File

@@ -0,0 +1,15 @@
<div class="pure-menu pure-menu-horizontal" style="height: 2em;">
<a href="#" class="pure-menu-heading pure-menu-link">gropple</a>
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a href="/" class="pure-menu-link">Home</a>
</li>
<li class="pure-menu-item">
<a href="/config" class="pure-menu-link">Config</a>
</li>
<li class="pure-menu-item">
<a href="https://github.com/tardisx/gropple" class="pure-menu-link">Github</a>
</li>
</ul>
</div>

View File

@@ -0,0 +1,80 @@
{{ define "content" }}
<div id="layout" class="pure-g pure-u-1" x-data="popup()" x-init="fetch_data()">
<h2>Download started</h2>
<p>Fetching <tt>{{ .dl.Url }}</tt></p>
<table class="pure-table" >
<tr>
<th>profile</th>
<td>{{ .dl.DownloadProfile.Name }}</td>
</tr>
<tr><th>current filename</th><td x-text="filename"></td></tr>
<tr>
<th>destination</th>
<td>
{{ if .dl.Destination }} {{ .dl.Destination.Name }} {{ else }} leave in {{ .config.Server.DownloadPath }} {{ end }}
</td>
</tr>
<tr><th>state</th><td x-text="state"></td></tr>
<tr x-show="playlist_total > 0"><th>playlist progress</th><td x-text="playlist_current + '/' + playlist_total"></td></tr>
<tr><th>progress</th><td x-text="percent"></td></tr>
<tr><th>ETA</th><td x-text="eta"></td></tr>
</table>
<p>You can close this window and your download will continue. Check the <a href="/" target="_gropple_status">Status page</a> to see all downloads in progress.</p>
{{ if .canStop }}
<button x-show="state=='Downloading'" class="pure-button" @click="stop()">stop</button>
{{ end }}
<div>
<h4>Logs</h4>
<pre x-text="log" style="height: auto;">
</pre>
</div>
</div>
{{ end }}
{{ define "js" }}
<script>
function popup() {
history.replaceState(null, '', ['/fetch/{{ .dl.Id }}'])
return {
eta: '', percent: 0.0, state: '??', filename: '', finished: false, log :'',
playlist_current: 0, playlist_total: 0,
stop() {
let op = {
method: 'POST',
body: JSON.stringify({action: 'stop'}),
headers: { 'Content-Type': 'application/json' }
};
fetch('/rest/fetch/{{ .dl.Id }}', op)
.then(response => response.json())
.then(info => {
console.log(info)
})
},
fetch_data() {
fetch('/rest/fetch/{{ .dl.Id }}')
.then(response => response.json())
.then(info => {
this.eta = info.eta;
this.percent = info.percent + "%";
this.state = info.state;
this.playlist_current = info.playlist_current;
this.playlist_total = info.playlist_total;
this.finished = info.finished;
if (info.files && info.files.length > 0) {
this.filename = info.files[info.files.length - 1];
}
if (info.log && info.log.length > 0) {
this.log = info.log.join("\n");
}
console.log('finish?', this.finished);
if (! this.finished) {
setTimeout(() => { this.fetch_data() }, 1000);
}
console.log('log', this.log);
});
},
}
}
</script>
{{ end }}

View File

@@ -0,0 +1,74 @@
{{ define "content" }}
<div id="layout" class="pure-g pure-u-1" x-data="popup_create()" >
<h2>Download create</h2>
<p>URL: <tt>{{ .url }}</tt></p>
<p class="error" x-show="error_message" x-transition.duration.500ms x-text="error_message"></p>
<table class="pure-table" >
<tr>
<th>profile</th>
<td>
<select class="pure-input-1-2" x-model="profile_chosen">
<option value="">choose a profile</option>
{{ range $i := .config.DownloadProfiles }}
<option>{{ $i.Name }}</option>
{{ end }}
</select>
</td>
</tr>
<tr>
<th>destination</th>
<td>
<select class="pure-input-1-2" x-model="destination_chosen">
<option value="">leave in {{ .config.Server.DownloadPath }}</option>
{{ range $i := .config.Destinations }}
<option>{{ $i.Name }}</option>
{{ end }}
</select>
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<button class="pure-button" @click="start()">start download</button>
</td>
</tr>
</table>
</div>
{{ end }}
{{ define "js" }}
<script>
function popup_create() {
return {
profile_chosen: "",
destination_chosen: "",
error_message: "",
start() {
let op = {
method: 'POST',
body: JSON.stringify({action: 'start', url: '{{ .url }}', profile: this.profile_chosen, destination: this.destination_chosen}),
headers: { 'Content-Type': 'application/json' }
};
fetch('/fetch', op)
.then(response => response.json())
.then(response => {
console.log(response)
if (response.error) {
this.error_message = response.error;
this.success_message = '';
document.body.scrollTop = document.documentElement.scrollTop = 0;
} else {
this.error_message = '';
console.log(response.location)
window.location = response.location
}
})
}
}
}
</script>
{{ end }}