mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 03:15:31 -04:00
Rewrite the download manager page
This commit is contained in:
parent
455315a362
commit
280490fb36
@ -1,28 +1,64 @@
|
|||||||
$(() => {
|
/**
|
||||||
$('input.uk-checkbox').each((i, e) => {
|
* Set an alpine.js property
|
||||||
$(e).change(() => {
|
*
|
||||||
loadConfig();
|
* @function setProp
|
||||||
});
|
* @param {string} key - Key of the data property
|
||||||
});
|
* @param {*} prop - The data property
|
||||||
loadConfig();
|
*/
|
||||||
load();
|
const setProp = (key, prop) => {
|
||||||
|
$('#root').get(0).__x.$data[key] = prop;
|
||||||
const intervalMS = 5000;
|
|
||||||
setTimeout(() => {
|
|
||||||
setInterval(() => {
|
|
||||||
if (globalConfig.autoRefresh !== true) return;
|
|
||||||
load();
|
|
||||||
}, intervalMS);
|
|
||||||
}, intervalMS);
|
|
||||||
});
|
|
||||||
var globalConfig = {};
|
|
||||||
var loading = false;
|
|
||||||
|
|
||||||
const loadConfig = () => {
|
|
||||||
globalConfig.autoRefresh = $('#auto-refresh').prop('checked');
|
|
||||||
};
|
};
|
||||||
const remove = (id) => {
|
|
||||||
var url = base_url + 'api/admin/mangadex/queue/delete';
|
/**
|
||||||
|
* Get an alpine.js property
|
||||||
|
*
|
||||||
|
* @function getProp
|
||||||
|
* @param {string} key - Key of the data property
|
||||||
|
* @return {*} The data property
|
||||||
|
*/
|
||||||
|
const getProp = (key) => {
|
||||||
|
return $('#root').get(0).__x.$data[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current queue and update the view
|
||||||
|
*
|
||||||
|
* @function load
|
||||||
|
*/
|
||||||
|
const load = () => {
|
||||||
|
try {
|
||||||
|
setProp('loading', true);
|
||||||
|
} catch {}
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: base_url + 'api/admin/mangadex/queue',
|
||||||
|
dataType: 'json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
if (!data.success && data.error) {
|
||||||
|
alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProp('jobs', data.jobs);
|
||||||
|
setProp('paused', data.paused);
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
setProp('loading', false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an action on either a specific job or the entire queue
|
||||||
|
*
|
||||||
|
* @function jobAction
|
||||||
|
* @param {string} action - The action to perform. Should be either 'delete' or 'retry'
|
||||||
|
* @param {string?} id - (Optional) A job ID. When omitted, apply the action to the queue
|
||||||
|
*/
|
||||||
|
const jobAction = (action, id) => {
|
||||||
|
let url = `${base_url}api/admin/mangadex/queue/${action}`;
|
||||||
if (id !== undefined)
|
if (id !== undefined)
|
||||||
url += '?' + $.param({
|
url += '?' + $.param({
|
||||||
id: id
|
id: id
|
||||||
@ -35,42 +71,24 @@ const remove = (id) => {
|
|||||||
})
|
})
|
||||||
.done(data => {
|
.done(data => {
|
||||||
if (!data.success && data.error) {
|
if (!data.success && data.error) {
|
||||||
alert('danger', `Failed to remove job from download queue. Error: ${data.error}`);
|
alert('danger', `Failed to ${action} job from download queue. Error: ${data.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to remove job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to ${action} job from download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
});
|
|
||||||
};
|
|
||||||
const refresh = (id) => {
|
|
||||||
var url = base_url + 'api/admin/mangadex/queue/retry';
|
|
||||||
if (id !== undefined)
|
|
||||||
url += '?' + $.param({
|
|
||||||
id: id
|
|
||||||
});
|
|
||||||
console.log(url);
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: url,
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
if (!data.success && data.error) {
|
|
||||||
alert('danger', `Failed to restart download job. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
load();
|
|
||||||
})
|
|
||||||
.fail((jqXHR, status) => {
|
|
||||||
alert('danger', `Failed to restart download job. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause/resume the download
|
||||||
|
*
|
||||||
|
* @function toggle
|
||||||
|
*/
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
$('#pause-resume-btn').attr('disabled', '');
|
setProp('toggling', true);
|
||||||
const paused = $('#pause-resume-btn').text() === 'Resume download';
|
const action = getProp('paused') ? 'resume' : 'pause';
|
||||||
const action = paused ? 'resume' : 'pause';
|
|
||||||
const url = `${base_url}api/admin/mangadex/queue/${action}`;
|
const url = `${base_url}api/admin/mangadex/queue/${action}`;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
@ -82,64 +100,47 @@ const toggle = () => {
|
|||||||
})
|
})
|
||||||
.always(() => {
|
.always(() => {
|
||||||
load();
|
load();
|
||||||
$('#pause-resume-btn').removeAttr('disabled');
|
setProp('toggling', false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const load = () => {
|
|
||||||
if (loading) return;
|
|
||||||
loading = true;
|
|
||||||
console.log('fetching');
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: base_url + 'api/admin/mangadex/queue',
|
|
||||||
dataType: 'json'
|
|
||||||
})
|
|
||||||
.done(data => {
|
|
||||||
if (!data.success && data.error) {
|
|
||||||
alert('danger', `Failed to fetch download queue. Error: ${data.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(data);
|
|
||||||
const btnText = data.paused ? "Resume download" : "Pause download";
|
|
||||||
$('#pause-resume-btn').text(btnText);
|
|
||||||
$('#pause-resume-btn').removeAttr('hidden');
|
|
||||||
const rows = data.jobs.map(obj => {
|
|
||||||
var cls = 'label ';
|
|
||||||
if (obj.status === 'Pending')
|
|
||||||
cls += 'label-pending';
|
|
||||||
if (obj.status === 'Completed')
|
|
||||||
cls += 'label-success';
|
|
||||||
if (obj.status === 'Error')
|
|
||||||
cls += 'label-danger';
|
|
||||||
if (obj.status === 'MissingPages')
|
|
||||||
cls += 'label-warning';
|
|
||||||
|
|
||||||
const info = obj.status_message.length > 0 ? '<span uk-icon="info"></span>' : '';
|
/**
|
||||||
const statusSpan = `<span class="${cls}">${obj.status} ${info}</span>`;
|
* Get the uk-label class name for a given job status
|
||||||
const dropdown = obj.status_message.length > 0 ? `<div uk-dropdown>${obj.status_message}</div>` : '';
|
*
|
||||||
const retryBtn = obj.status_message.length > 0 ? `<a onclick="refresh('${obj.id}')" uk-icon="refresh"></a>` : '';
|
* @function statusClass
|
||||||
return `<tr id="chapter-${obj.id}">
|
* @param {string} status - The job status
|
||||||
<td>${obj.plugin_id ? obj.title : `<a href="${baseURL}/chapter/${obj.id}">${obj.title}</a>`}</td>
|
* @return {string} The class name string
|
||||||
<td>${obj.plugin_id ? obj.manga_title : `<a href="${baseURL}/manga/${obj.manga_id}">${obj.manga_title}</a>`}</td>
|
*/
|
||||||
<td>${obj.success_count}/${obj.pages}</td>
|
const statusClass = status => {
|
||||||
<td>${moment(obj.time).fromNow()}</td>
|
let cls = 'label ';
|
||||||
<td>${statusSpan} ${dropdown}</td>
|
switch (status) {
|
||||||
<td>${obj.plugin_id || ""}</td>
|
case 'Pending':
|
||||||
<td>
|
cls += 'label-pending';
|
||||||
<a onclick="remove('${obj.id}')" uk-icon="trash"></a>
|
break;
|
||||||
${retryBtn}
|
case 'Completed':
|
||||||
</td>
|
cls += 'label-success';
|
||||||
</tr>`;
|
break;
|
||||||
});
|
case 'Error':
|
||||||
|
cls += 'label-danger';
|
||||||
const tbody = `<tbody>${rows.join('')}</tbody>`;
|
break;
|
||||||
$('tbody').remove();
|
case 'MissingPages':
|
||||||
$('table').append(tbody);
|
cls += 'label-warning';
|
||||||
})
|
break;
|
||||||
.fail((jqXHR, status) => {
|
}
|
||||||
alert('danger', `Failed to fetch download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
return cls;
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
const ws = new WebSocket(`ws://${location.host}/api/admin/mangadex/queue`);
|
||||||
|
ws.onmessage = event => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
setProp('jobs', data.jobs);
|
||||||
|
setProp('paused', data.paused);
|
||||||
|
};
|
||||||
|
ws.onerror = err => {
|
||||||
|
alert('danger', `Socket connection failed. Error: ${err}`);
|
||||||
|
};
|
||||||
|
ws.onclose = err => {
|
||||||
|
alert('danger', 'Socket connection failed');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
@ -212,6 +212,18 @@ class APIRouter < Router
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ws "/api/admin/mangadex/queue" do |socket, env|
|
||||||
|
interval_raw = env.params.query["interval"]?
|
||||||
|
interval = (interval_raw.to_i? if interval_raw) || 5
|
||||||
|
loop do
|
||||||
|
socket.send({
|
||||||
|
"jobs" => @context.queue.get_all,
|
||||||
|
"paused" => @context.queue.paused?,
|
||||||
|
}.to_json)
|
||||||
|
sleep interval.seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "/api/admin/mangadex/queue" do |env|
|
get "/api/admin/mangadex/queue" do |env|
|
||||||
begin
|
begin
|
||||||
jobs = @context.queue.get_all
|
jobs = @context.queue.get_all
|
||||||
|
@ -1,32 +1,68 @@
|
|||||||
<div class="uk-margin">
|
<div id="root" x-data="{jobs: [], paused: undefined, loading: false, toggling: false}" x-init="load()">
|
||||||
<div id="actions" class="uk-margin">
|
<div class="uk-margin">
|
||||||
<button class="uk-button uk-button-default" onclick="remove()">Delete Completed Tasks</button>
|
<button class="uk-button uk-button-default" @click="jobAction('delete')">Delete Completed Tasks</button>
|
||||||
<button class="uk-button uk-button-default" onclick="refresh()">Retry Failed Tasks</button>
|
<button class="uk-button uk-button-default" @click="jobAction('retry')">Retry Failed Tasks</button>
|
||||||
<button class="uk-button uk-button-default" onclick="load()">Refresh Queue</button>
|
<button class="uk-button uk-button-default" @click="load()" :disabled="loading ? '' : false">Refresh Queue</button>
|
||||||
<button class="uk-button uk-button-default" onclick="toggle()" id="pause-resume-btn" hidden></button>
|
<button class="uk-button uk-button-default" x-show="paused !== undefined" x-text="paused ? 'Resume Download' : 'Pause Download'" @click="toggle()" :disabled="toggling ? '' : false"></button>
|
||||||
</div>
|
|
||||||
<div id="config" class="uk-margin">
|
|
||||||
<label><input id="auto-refresh" class="uk-checkbox" type="checkbox" checked> Auto Refresh</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<table class="uk-table uk-table-striped uk-overflow-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Chapter</th>
|
||||||
|
<th>Manga</th>
|
||||||
|
<th>Progress</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Plugin</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="job in jobs" :key="job">
|
||||||
|
<tr :id="`chapter-${job.id}`">
|
||||||
|
|
||||||
|
<template x-if="job.plugin_id">
|
||||||
|
<td x-text="job.title"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="!job.plugin_id">
|
||||||
|
<td><a :href="`${'<%= mangadex_base_url %>'.replace(/\/$/, '')}/chapter/${job.id}`" x-text="job.title"></td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="job.plugin_id">
|
||||||
|
<td x-text="job.manga_title"></td>
|
||||||
|
</template>
|
||||||
|
<template x-if="!job.plugin_id">
|
||||||
|
<td><a :href="`${'<%= mangadex_base_url %>'.replace(/\/$/, '')}/manga/${job.manga_id}`" x-text="job.manga_title"></td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<td x-text="`${job.success_count}/${job.pages}`"></td>
|
||||||
|
<td x-text="`${moment(job.time).fromNow()}`"></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span :class="statusClass(job.status)" x-text="job.status"></span>
|
||||||
|
<template x-if="job.status_message.length > 0">
|
||||||
|
<div class="uk-inline">
|
||||||
|
<span uk-icon="info"></span>
|
||||||
|
<div uk-dropdown x-text="job.status_message"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td x-text="`${job.plugin_id || ''}`"></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a :onclick="`jobAction('delete', '${job.id}')`" uk-icon="trash"></a>
|
||||||
|
<template x-if="job.status_message.length > 0">
|
||||||
|
<a :onclick="`jobAction('retry', '${job.id}')`" uk-icon="refresh"></a>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<table class="uk-table uk-table-striped uk-overflow-auto">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Chapter</th>
|
|
||||||
<th>Manga</th>
|
|
||||||
<th>Progress</th>
|
|
||||||
<th>Time</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Plugin</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script>
|
|
||||||
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
|
|
||||||
</script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/download-manager.js"></script>
|
<script src="<%= base_url %>js/download-manager.js"></script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user