Move JS files to /static/js/
This commit is contained in:
211
app/public/static/js/collection_editor.js
Normal file
211
app/public/static/js/collection_editor.js
Normal file
@@ -0,0 +1,211 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
function updateOrder() {
|
||||
const elements = [...document.querySelector(".sortable").children];
|
||||
const ids = elements
|
||||
.filter(x => !x.classList.contains("d-none"))
|
||||
.map(x => x.dataset.id)
|
||||
.filter(x => x);
|
||||
|
||||
document.querySelector("input[name='order']").value = ids.join(",");
|
||||
}
|
||||
|
||||
|
||||
function removePackage(card) {
|
||||
const message = document.getElementById("confirm_delete").innerText.trim();
|
||||
const title = card.querySelector("h5 a").innerText.trim();
|
||||
if (!confirm(message.replace("{title}", title))) {
|
||||
return;
|
||||
}
|
||||
|
||||
card.querySelector("input[name^=package_removed]").value = "1";
|
||||
card.classList.add("d-none");
|
||||
onPackageQueryUpdate();
|
||||
updateOrder();
|
||||
}
|
||||
|
||||
|
||||
function restorePackage(id) {
|
||||
const idElement = document.querySelector(`[value='${id}']`);
|
||||
if (!idElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const card = idElement.parentNode.parentNode.parentNode.parentNode;
|
||||
console.assert(card.classList.contains("card"));
|
||||
|
||||
card.classList.remove("d-none");
|
||||
card.querySelector("input[name^=package_removed]").value = "0";
|
||||
card.scrollIntoView();
|
||||
onPackageQueryUpdate();
|
||||
updateOrder();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function getAddedPackages() {
|
||||
const ids = document.querySelectorAll("#package_list > article:not(.d-none) input[name^=package_ids]");
|
||||
return [...ids].map(x => x.value);
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
|
||||
function addPackage(pkg) {
|
||||
document.getElementById("add_package").value = "";
|
||||
document.getElementById("add_package_results").innerHTML = "";
|
||||
|
||||
const id = `${pkg.author}/${pkg.name}`;
|
||||
if (restorePackage(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextId = document.querySelectorAll("input[name^=package_ids-]").length;
|
||||
const url = `/packages/${id}/`;
|
||||
const temp = document.createElement("div");
|
||||
temp.innerHTML = `
|
||||
<article class="card my-3" data-id="${escapeHtml(id)}">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-auto text-muted pe-2">
|
||||
<i class="fas fa-bars"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="btn btn-sm btn-danger remove-package float-end" type="button" aria-label="Remove">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<h5>
|
||||
<a href="${escapeHtml(url)}" target="_blank">
|
||||
${escapeHtml(pkg.title)} by ${escapeHtml(pkg.author)}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="text-muted">
|
||||
${escapeHtml(pkg.short_description)}
|
||||
</p>
|
||||
<input id="package_ids-${nextId}" name="package_ids-${nextId}" type="hidden" value="${id}">
|
||||
<input id="package_removed-${nextId}" name="package_removed-${nextId}" type="hidden" value="0">
|
||||
<div>
|
||||
<label for="descriptions-${nextId}" class="form-label">Short Description</label>
|
||||
<input class="form-control" id="descriptions-${nextId}" maxlength="500" minlength="0"
|
||||
name="descriptions-${nextId}" type="text" value="">
|
||||
<small class="form-text text-muted">You can replace the description with your own</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
const card = temp.children[0];
|
||||
document.getElementById("package_list").appendChild(card);
|
||||
card.scrollIntoView();
|
||||
|
||||
const button = card.querySelector(".btn-danger");
|
||||
button.addEventListener("click", () => removePackage(card));
|
||||
|
||||
updateOrder();
|
||||
}
|
||||
|
||||
|
||||
function updateResults(packages) {
|
||||
const results = document.getElementById("add_package_results");
|
||||
results.innerHTML = "";
|
||||
document.getElementById("add_package_empty").style.display = packages.length === 0 ? "block" : "none";
|
||||
|
||||
const alreadyAdded = getAddedPackages();
|
||||
packages.slice(0, 5).forEach(pkg => {
|
||||
const result = document.createElement("a");
|
||||
result.classList.add("list-group-item");
|
||||
result.classList.add("list-group-item-action");
|
||||
result.innerText = `${pkg.title} by ${pkg.author}`;
|
||||
if (alreadyAdded.includes(`${pkg.author}/${pkg.name}`)) {
|
||||
result.classList.add("active");
|
||||
result.innerHTML = "<i class='fas fa-check me-3 text-success'></i>" + result.innerHTML;
|
||||
}
|
||||
result.addEventListener("click", () => addPackage(pkg));
|
||||
results.appendChild(result);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let currentRequestId;
|
||||
|
||||
async function fetchPackagesAndUpdateResults(query) {
|
||||
const requestId = Math.random() * 1000000;
|
||||
currentRequestId = requestId;
|
||||
if (query === "") {
|
||||
updateResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL("/api/packages/", window.location.origin);
|
||||
url.searchParams.set("q", query);
|
||||
const resp = await fetch(url.toString());
|
||||
if (!resp.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packages = await resp.json();
|
||||
if (currentRequestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateResults(packages);
|
||||
}
|
||||
|
||||
|
||||
let timeoutHandle;
|
||||
function onPackageQueryUpdate() {
|
||||
const query = document.getElementById("add_package").value.trim();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
timeoutHandle = setTimeout(
|
||||
() => fetchPackagesAndUpdateResults(query).catch(console.error),
|
||||
200);
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.querySelectorAll(".remove-package").forEach(button => {
|
||||
const card = button.parentNode.parentNode.parentNode.parentNode;
|
||||
console.assert(card.classList.contains("card"));
|
||||
|
||||
const field = card.querySelector("input[name^=package_removed]");
|
||||
|
||||
// Reloading/validation errors will cause this to be 1 at load
|
||||
if (field && field.value === "1") {
|
||||
card.classList.add("d-none");
|
||||
} else {
|
||||
button.addEventListener("click", () => removePackage(card));
|
||||
}
|
||||
});
|
||||
|
||||
const addPackageQuery = document.getElementById("add_package");
|
||||
addPackageQuery.value = "";
|
||||
addPackageQuery.classList.remove("d-none");
|
||||
addPackageQuery.addEventListener("input", onPackageQueryUpdate);
|
||||
addPackageQuery.addEventListener('keydown',(e)=>{
|
||||
if (e.key === "Enter") {
|
||||
onPackageQueryUpdate();
|
||||
e.preventDefault();
|
||||
}
|
||||
})
|
||||
|
||||
updateOrder();
|
||||
$(".sortable").sortable({
|
||||
update: updateOrder,
|
||||
});
|
||||
});
|
||||
58
app/public/static/js/markdowntextarea.js
Normal file
58
app/public/static/js/markdowntextarea.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
document.querySelectorAll("textarea.markdown").forEach((element) => {
|
||||
async function render(plainText, preview) {
|
||||
const response = await fetch(new Request("/api/markdown/", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: plainText,
|
||||
headers: {
|
||||
"Accept": "text/html; charset=UTF-8",
|
||||
},
|
||||
}));
|
||||
|
||||
preview.innerHTML = await response.text();
|
||||
}
|
||||
|
||||
let timeout_id = null;
|
||||
element.easy_mde = new EasyMDE({
|
||||
element: element,
|
||||
hideIcons: ["image"],
|
||||
showIcons: ["code", "table"],
|
||||
forceSync: true,
|
||||
toolbar: [
|
||||
"bold",
|
||||
"italic",
|
||||
"heading",
|
||||
"|",
|
||||
"code",
|
||||
"quote",
|
||||
"unordered-list",
|
||||
"ordered-list",
|
||||
"|",
|
||||
"link",
|
||||
"table",
|
||||
"|",
|
||||
"preview",
|
||||
"side-by-side",
|
||||
"fullscreen",
|
||||
"|",
|
||||
"guide",
|
||||
],
|
||||
previewRender: (plainText, preview) => {
|
||||
if (timeout_id) {
|
||||
clearTimeout(timeout_id);
|
||||
}
|
||||
|
||||
timeout_id = setTimeout(() => {
|
||||
render(plainText, preview).catch(console.error);
|
||||
timeout_id = null;
|
||||
}, 500);
|
||||
|
||||
return preview.innerHTML;
|
||||
}
|
||||
});
|
||||
})
|
||||
306
app/public/static/js/package_charts.js
Normal file
306
app/public/static/js/package_charts.js
Normal file
@@ -0,0 +1,306 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
const labelColor = "#bbb";
|
||||
const annotationColor = "#bbb";
|
||||
const annotationLabelBgColor = "#444";
|
||||
const gridColor = "#333";
|
||||
|
||||
|
||||
const chartColors = [
|
||||
"#7eb26d",
|
||||
"#eab839",
|
||||
"#6ed0e0",
|
||||
"#e24d42",
|
||||
"#1f78c1",
|
||||
"#ba43a9",
|
||||
];
|
||||
|
||||
|
||||
const annotationNov5 = {
|
||||
type: "line",
|
||||
borderColor: annotationColor,
|
||||
borderWidth: 1,
|
||||
click: function({chart, element}) {
|
||||
document.location = "https://fosstodon.org/@rubenwardy/109303281233703275";
|
||||
},
|
||||
label: {
|
||||
backgroundColor: annotationLabelBgColor,
|
||||
content: "YouTube Video",
|
||||
display: true,
|
||||
position: "end",
|
||||
color: "#00bc8c",
|
||||
rotation: "auto",
|
||||
backgroundShadowColor: "rgba(0, 0, 0, 0.4)",
|
||||
shadowBlur: 3,
|
||||
},
|
||||
scaleID: "x",
|
||||
value: "2022-11-05",
|
||||
};
|
||||
|
||||
|
||||
function hexToRgb(hex) {
|
||||
var bigint = parseInt(hex, 16);
|
||||
var r = (bigint >> 16) & 255;
|
||||
var g = (bigint >> 8) & 255;
|
||||
var b = bigint & 255;
|
||||
|
||||
return r + "," + g + "," + b;
|
||||
}
|
||||
|
||||
|
||||
function sum(list) {
|
||||
return list.reduce((acc, x) => acc + x, 0);
|
||||
}
|
||||
|
||||
|
||||
const chartColorsBg = chartColors.map(color => `rgba(${hexToRgb(color.slice(1))}, 0.2)`);
|
||||
|
||||
const SECONDS_IN_A_DAY = 1000 * 3600 * 24;
|
||||
|
||||
|
||||
function format_message(id, values) {
|
||||
let format = document.getElementById(id).textContent;
|
||||
values.forEach((value, i) => {
|
||||
format = format.replace("$" + (i + 1), value);
|
||||
})
|
||||
return format;
|
||||
}
|
||||
|
||||
|
||||
function add_summary_card(title, icon, value, extra) {
|
||||
const ele = document.createElement("div");
|
||||
ele.innerHTML = `
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body align-items-center text-center">
|
||||
<div class="mt-0 mb-3">
|
||||
<i class="fas fa-${icon} me-1"></i>
|
||||
<span class="summary-title"></span>
|
||||
</div>
|
||||
<div class="my-0 h4">
|
||||
<span class="summary-value"></span>
|
||||
<small class="text-muted ms-2 summary-extra"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
ele.querySelector(".summary-title").textContent = title;
|
||||
ele.querySelector(".summary-value").textContent = value;
|
||||
ele.querySelector(".summary-extra").textContent = extra;
|
||||
|
||||
document.getElementById("stats-summaries").appendChild(ele.children[0]);
|
||||
}
|
||||
|
||||
async function load_data() {
|
||||
const root = document.getElementById("stats-root");
|
||||
const source = root.getAttribute("data-source");
|
||||
const is_range = root.getAttribute("data-is-range") == "true";
|
||||
const response = await fetch(source);
|
||||
const json = await response.json();
|
||||
|
||||
document.getElementById("loading").style.display = "none";
|
||||
|
||||
if (json == null) {
|
||||
document.getElementById("empty-view").style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date(json.start);
|
||||
const endDate = new Date(json.end);
|
||||
const numberOfDays = Math.round((endDate.valueOf() - startDate.valueOf()) / SECONDS_IN_A_DAY) + 1;
|
||||
const dates = [...Array(numberOfDays)].map((_, i) => {
|
||||
const date = new Date(startDate.valueOf() + i*SECONDS_IN_A_DAY);
|
||||
return date.toISOString().split("T")[0];
|
||||
});
|
||||
|
||||
if (!is_range) {
|
||||
if (json.platform_minetest.length >= 30) {
|
||||
const total30 = sum(json.platform_minetest.slice(-30)) + sum(json.platform_other.slice(-30));
|
||||
add_summary_card(format_message("downloads-30days", []), "download", total30,
|
||||
format_message("downloads-per-day", [ (total30 / 30).toFixed(0) ]));
|
||||
}
|
||||
|
||||
const total7 = sum(json.platform_minetest.slice(-7)) + sum(json.platform_other.slice(-7));
|
||||
add_summary_card(format_message("downloads-7days", []), "download", total7,
|
||||
format_message("downloads-per-day", [ (total7 / 7).toFixed(0) ]));
|
||||
} else {
|
||||
const total = sum(json.platform_minetest) + sum(json.platform_other);
|
||||
const days = Math.max(json.platform_minetest.length, json.platform_other.length);
|
||||
const title = format_message("downloads-range", [ json.start, json.end ]);
|
||||
add_summary_card(title, "download", total,
|
||||
format_message("downloads-per-day", [ (total / days).toFixed(0) ]));
|
||||
}
|
||||
|
||||
const jsonOther = json.platform_minetest.map((value, i) =>
|
||||
value + json.platform_other[i]
|
||||
- json.reason_new[i] - json.reason_dependency[i]
|
||||
- json.reason_update[i]);
|
||||
|
||||
root.style.display = "block";
|
||||
|
||||
function getData(list) {
|
||||
return list.map((value, i) => ({ x: dates[i], y: value }));
|
||||
}
|
||||
|
||||
const annotations = {};
|
||||
if (new Date(json.start) < new Date("2022-11-05")) {
|
||||
annotations.annotationNov5 = annotationNov5;
|
||||
}
|
||||
|
||||
if (json.package_downloads) {
|
||||
const packageRecentDownloads = Object.fromEntries(Object.entries(json.package_downloads)
|
||||
.map(([label, values]) => [label, sum(values.slice(-30))]));
|
||||
|
||||
document.getElementById("downloads-by-package").classList.remove("d-none");
|
||||
const ctx = document.getElementById("chart-packages").getContext("2d");
|
||||
|
||||
const data = {
|
||||
datasets: Object.entries(json.package_downloads)
|
||||
.sort((a, b) => packageRecentDownloads[a[0]] - packageRecentDownloads[b[0]])
|
||||
.map(([label, values]) => ({ label, data: getData(values) })),
|
||||
};
|
||||
setup_chart(ctx, data, annotations);
|
||||
}
|
||||
|
||||
{
|
||||
const ctx = document.getElementById("chart-platform").getContext("2d");
|
||||
const data = {
|
||||
datasets: [
|
||||
{ label: "Web / other", data: getData(json.platform_other) },
|
||||
{ label: "Minetest", data: getData(json.platform_minetest) },
|
||||
],
|
||||
};
|
||||
setup_chart(ctx, data, annotations);
|
||||
}
|
||||
|
||||
{
|
||||
const ctx = document.getElementById("chart-reason").getContext("2d");
|
||||
const data = {
|
||||
datasets: [
|
||||
{ label: "Other / Unknown", data: getData(jsonOther) },
|
||||
{ label: "Update", data: getData(json.reason_update) },
|
||||
{ label: "Dependency", data: getData(json.reason_dependency) },
|
||||
{ label: "New Install", data: getData(json.reason_new) },
|
||||
],
|
||||
};
|
||||
setup_chart(ctx, data, annotations);
|
||||
}
|
||||
|
||||
{
|
||||
const ctx = document.getElementById("chart-reason-pie").getContext("2d");
|
||||
const data = {
|
||||
labels: [
|
||||
"New Install",
|
||||
"Dependency",
|
||||
"Update",
|
||||
"Other / Unknown",
|
||||
],
|
||||
datasets: [{
|
||||
label: "My First Dataset",
|
||||
data: [
|
||||
sum(json.reason_new),
|
||||
sum(json.reason_dependency),
|
||||
sum(json.reason_update),
|
||||
sum(jsonOther),
|
||||
],
|
||||
backgroundColor: chartColors,
|
||||
hoverOffset: 4,
|
||||
borderWidth: 0,
|
||||
}]
|
||||
};
|
||||
const config = {
|
||||
type: "doughnut",
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: labelColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
new Chart(ctx, config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setup_chart(ctx, data, annotations) {
|
||||
data.datasets = data.datasets.map((set, i) => {
|
||||
const colorIdx = (data.datasets.length - i - 1) % chartColors.length;
|
||||
return {
|
||||
fill: true,
|
||||
backgroundColor: chartColorsBg[colorIdx],
|
||||
borderColor: chartColors[colorIdx],
|
||||
pointBackgroundColor: chartColors[colorIdx],
|
||||
...set,
|
||||
};
|
||||
});
|
||||
|
||||
const config = {
|
||||
type: "line",
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: "index"
|
||||
},
|
||||
|
||||
legend: {
|
||||
reverse: true,
|
||||
labels: {
|
||||
color: labelColor,
|
||||
}
|
||||
},
|
||||
|
||||
annotation: {
|
||||
annotations,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
mode: "nearest",
|
||||
axis: "x",
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
// min: start,
|
||||
// max: end,
|
||||
unit: "day",
|
||||
},
|
||||
ticks: {
|
||||
color: labelColor,
|
||||
},
|
||||
grid: {
|
||||
color: gridColor,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
min: 0,
|
||||
precision: 0,
|
||||
ticks: {
|
||||
color: labelColor,
|
||||
},
|
||||
grid: {
|
||||
color: gridColor,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
new Chart(ctx, config);
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", load_data);
|
||||
74
app/public/static/js/package_create.js
Normal file
74
app/public/static/js/package_create.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
function hide(sel) {
|
||||
document.querySelectorAll(sel).forEach(x => x.classList.add("d-none"));
|
||||
}
|
||||
|
||||
function show(sel) {
|
||||
document.querySelectorAll(sel).forEach(x => x.classList.remove("d-none"));
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
function finish() {
|
||||
hide(".pkg_wiz_1");
|
||||
hide(".pkg_wiz_2");
|
||||
show(".pkg_repo");
|
||||
show(".pkg_meta");
|
||||
}
|
||||
|
||||
hide(".pkg_meta");
|
||||
show(".pkg_wiz_1");
|
||||
|
||||
document.getElementById("pkg_wiz_1_skip").addEventListener("click", finish);
|
||||
document.getElementById("pkg_wiz_1_next").addEventListener("click", () => {
|
||||
const repoURL = document.getElementById("repo").value;
|
||||
if (repoURL.trim() !== "") {
|
||||
hide(".pkg_wiz_1");
|
||||
show(".pkg_wiz_2");
|
||||
hide(".pkg_repo");
|
||||
|
||||
function setField(sel, value) {
|
||||
if (value && value !== "") {
|
||||
const ele = document.querySelector(sel);
|
||||
ele.value = value;
|
||||
ele.dispatchEvent(new Event("change"));
|
||||
|
||||
// EasyMDE doesn't always refresh the codemirror correctly
|
||||
if (ele.easy_mde) {
|
||||
setTimeout(() => {
|
||||
ele.easy_mde.value(value);
|
||||
ele.easy_mde.codemirror.refresh()
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
||||
setField("#name", result.name);
|
||||
setField("#title", result.title);
|
||||
setField("#repo", result.repo || repoURL);
|
||||
setField("#issueTracker", result.issueTracker);
|
||||
setField("#desc", result.desc);
|
||||
setField("#short_desc", result.short_desc);
|
||||
setField("#forums", result.forums);
|
||||
if (result.type && result.type.length > 2) {
|
||||
setField("[name='type']", result.type);
|
||||
}
|
||||
|
||||
finish();
|
||||
}).catch(function(e) {
|
||||
alert(e);
|
||||
show(".pkg_wiz_1");
|
||||
hide(".pkg_wiz_2");
|
||||
show(".pkg_repo");
|
||||
// finish()
|
||||
});
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
})
|
||||
84
app/public/static/js/package_edit.js
Normal file
84
app/public/static/js/package_edit.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
function hide(sel) {
|
||||
document.querySelectorAll(sel).forEach(x => x.classList.add("d-none"));
|
||||
}
|
||||
|
||||
function show(sel) {
|
||||
document.querySelectorAll(sel).forEach(x => x.classList.remove("d-none"));
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const typeEle = document.getElementById("type");
|
||||
typeEle.addEventListener("change", () => {
|
||||
show(".not_mod, .not_game, .not_txp");
|
||||
hide(".not_" + typeEle.value.toLowerCase());
|
||||
})
|
||||
show(".not_mod, .not_game, .not_txp");
|
||||
hide(".not_" + typeEle.value.toLowerCase());
|
||||
|
||||
const forumsField = document.getElementById("forums");
|
||||
forumsField.addEventListener("paste", function(e) {
|
||||
try {
|
||||
const pasteData = e.clipboardData.getData('text');
|
||||
const url = new URL(pasteData);
|
||||
if (url.hostname === "forum.minetest.net") {
|
||||
forumsField.value = url.searchParams.get("t");
|
||||
e.preventDefault();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Not a URL");
|
||||
}
|
||||
});
|
||||
|
||||
const openForums = document.getElementById("forums-button");
|
||||
openForums.addEventListener("click", () => {
|
||||
window.open("https://forum.minetest.net/viewtopic.php?t=" + forumsField.value, "_blank");
|
||||
});
|
||||
|
||||
let hint = null;
|
||||
function showHint(ele, text) {
|
||||
if (hint) {
|
||||
hint.remove();
|
||||
}
|
||||
|
||||
hint = document.createElement("div");
|
||||
hint.classList.add("alert");
|
||||
hint.classList.add("alert-warning");
|
||||
hint.classList.add("my-1");
|
||||
hint.innerHTML = text;
|
||||
|
||||
ele.parentNode.appendChild(hint);
|
||||
}
|
||||
|
||||
let hint_mtmods = `Tip:
|
||||
Don't include <i>Minetest</i>, <i>mod</i>, or <i>modpack</i> anywhere in the short description.
|
||||
It is unnecessary and wastes characters.`;
|
||||
|
||||
let hint_thegame = `Tip:
|
||||
It's obvious that this adds something to Minetest,
|
||||
there's no need to use phrases such as \"adds X to the game\".`;
|
||||
|
||||
const shortDescField = document.getElementById("short_desc");
|
||||
|
||||
function handleShortDescChange() {
|
||||
const val = shortDescField.value.toLowerCase();
|
||||
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
||||
showHint(shortDescField, hint_mtmods);
|
||||
} else if (val.indexOf("the game") >= 0) {
|
||||
showHint(shortDescField, hint_thegame);
|
||||
} else if (hint) {
|
||||
hint.remove();
|
||||
hint = null;
|
||||
}
|
||||
}
|
||||
|
||||
shortDescField.addEventListener("change", handleShortDescChange);
|
||||
shortDescField.addEventListener("paste", handleShortDescChange);
|
||||
shortDescField.addEventListener("keyup", handleShortDescChange);
|
||||
})
|
||||
64
app/public/static/js/polltask.js
Normal file
64
app/public/static/js/polltask.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
function getJSON(url, method) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(new Request(url, {
|
||||
method: method || "get",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
},
|
||||
})).then((response) => {
|
||||
response.text().then((txt) => {
|
||||
resolve(JSON.parse(txt))
|
||||
}).catch(reject)
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
function pollTask(poll_url, disableTimeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tries = 0;
|
||||
|
||||
function retry() {
|
||||
tries++;
|
||||
if (!disableTimeout && tries > 30) {
|
||||
reject("timeout")
|
||||
} else {
|
||||
const interval = Math.min(tries*100, 1000)
|
||||
console.log("Polling task in " + interval + "ms")
|
||||
setTimeout(step, interval)
|
||||
}
|
||||
}
|
||||
function step() {
|
||||
getJSON(poll_url).then((res) => {
|
||||
if (res.status === "SUCCESS") {
|
||||
console.log("Got result")
|
||||
resolve(res.result)
|
||||
} else if (res.status === "FAILURE" || res.status === "REVOKED") {
|
||||
reject(res.error || "Unknown server error")
|
||||
} else {
|
||||
retry()
|
||||
}
|
||||
}).catch(retry)
|
||||
}
|
||||
retry()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function performTask(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getJSON(url, "post").then((startResult) => {
|
||||
console.log(startResult)
|
||||
if (typeof startResult.poll_url == "string") {
|
||||
pollTask(startResult.poll_url).then(resolve).catch(reject)
|
||||
} else {
|
||||
reject("Start task didn't return string!")
|
||||
}
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
87
app/public/static/js/quick_review_voting.js
Normal file
87
app/public/static/js/quick_review_voting.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
function getVoteCount(button) {
|
||||
const badge = button.querySelector(".badge");
|
||||
return badge ? parseInt(badge.textContent) : 0;
|
||||
}
|
||||
|
||||
function setVoteCount(button, count) {
|
||||
let badge = button.querySelector(".badge");
|
||||
if (count == 0) {
|
||||
if (badge) {
|
||||
badge.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!badge) {
|
||||
badge = document.createElement("span")
|
||||
badge.classList.add("badge");
|
||||
badge.classList.add("bg-light");
|
||||
badge.classList.add("text-dark");
|
||||
badge.classList.add("ms-1");
|
||||
button.appendChild(badge);
|
||||
}
|
||||
|
||||
badge.textContent = count.toString();
|
||||
}
|
||||
|
||||
|
||||
async function submitForm(form, is_helpful) {
|
||||
const data = new URLSearchParams();
|
||||
for (const pair of new FormData(form)) {
|
||||
data.append(pair[0], pair[1]);
|
||||
}
|
||||
data.set("is_positive", is_helpful ? "yes" : "no");
|
||||
|
||||
const res = await fetch(form.getAttribute("action"), {
|
||||
method: "post",
|
||||
body: data,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.querySelectorAll(".review-helpful-vote").forEach((helpful_form) => {
|
||||
const yes = helpful_form.querySelector("button[name='is_positive'][value='yes']");
|
||||
const no = helpful_form.querySelector("button[name='is_positive'][value='no']");
|
||||
|
||||
function setVote(is_helpful) {
|
||||
const selected = is_helpful ? yes : no;
|
||||
const not_selected = is_helpful ? no : yes;
|
||||
|
||||
if (not_selected.classList.contains("btn-primary")) {
|
||||
setVoteCount(not_selected, Math.max(getVoteCount(not_selected) - 1, 0));
|
||||
}
|
||||
if (selected.classList.contains("btn-secondary")) {
|
||||
setVoteCount(selected, getVoteCount(selected) + 1);
|
||||
}
|
||||
|
||||
selected.classList.add("btn-primary");
|
||||
selected.classList.remove("btn-secondary");
|
||||
not_selected.classList.add("btn-secondary");
|
||||
not_selected.classList.remove("btn-primary");
|
||||
|
||||
submitForm(helpful_form, is_helpful).catch(console.error);
|
||||
}
|
||||
|
||||
yes.addEventListener("click", (e) => {
|
||||
setVote(true);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
no.addEventListener("click", (e) => {
|
||||
setVote(false)
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
});
|
||||
28
app/public/static/js/release_bulk_change.js
Normal file
28
app/public/static/js/release_bulk_change.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
function setup_toggle(type) {
|
||||
const toggle = document.getElementById("set_" + type);
|
||||
|
||||
function on_change() {
|
||||
const rel = document.getElementById(type + "_rel");
|
||||
if (toggle.checked) {
|
||||
rel.parentElement.style.opacity = "1";
|
||||
} else {
|
||||
// $("#" + type + "_rel").attr("disabled", "disabled");
|
||||
rel.parentElement.style.opacity = "0.4";
|
||||
rel.value = document.querySelector(`#${type}_rel option:first-child`).value;
|
||||
rel.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
|
||||
toggle.addEventListener("change", on_change);
|
||||
on_change();
|
||||
}
|
||||
|
||||
setup_toggle("min");
|
||||
setup_toggle("max");
|
||||
});
|
||||
25
app/public/static/js/release_minmax.js
Normal file
25
app/public/static/js/release_minmax.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const min = document.getElementById("min_rel");
|
||||
const max = document.getElementById("max_rel");
|
||||
const none = parseInt(document.querySelector("#min_rel option:first-child").value);
|
||||
const warning = document.getElementById("minmax_warning");
|
||||
|
||||
function ver_check() {
|
||||
const minv = parseInt(min.value);
|
||||
const maxv = parseInt(max.value);
|
||||
if (minv != none && maxv != none && minv > maxv) {
|
||||
warning.style.display = "block";
|
||||
} else {
|
||||
warning.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
min.addEventListener("change", ver_check);
|
||||
max.addEventListener("change", ver_check);
|
||||
ver_check();
|
||||
});
|
||||
19
app/public/static/js/release_new.js
Normal file
19
app/public/static/js/release_new.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
function check_opt() {
|
||||
if (document.querySelector("input[name='uploadOpt']:checked").value === "vcs") {
|
||||
document.getElementById("file_upload").parentElement.classList.add("d-none");
|
||||
document.getElementById("vcsLabel").parentElement.classList.remove("d-none");
|
||||
} else {
|
||||
document.getElementById("file_upload").parentElement.classList.remove("d-none");
|
||||
document.getElementById("vcsLabel").parentElement.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll("input[name='uploadOpt']").forEach(x => x.addEventListener("change", check_opt));
|
||||
check_opt();
|
||||
});
|
||||
17
app/public/static/js/screenshots_editor.js
Normal file
17
app/public/static/js/screenshots_editor.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
function update() {
|
||||
const elements = [...document.querySelector(".sortable").children];
|
||||
const ids = elements.map(x => x.dataset.id).filter(x => x);
|
||||
document.querySelector("input[name='order']").value = ids.join(",");
|
||||
}
|
||||
|
||||
update();
|
||||
$(".sortable").sortable({
|
||||
update: update
|
||||
});
|
||||
})
|
||||
146
app/public/static/js/tagselector.js
Normal file
146
app/public/static/js/tagselector.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/*!
|
||||
* Tag Selector plugin for jQuery: Facilitates selecting multiple tags by extending jQuery UI Autocomplete.
|
||||
* License: MIT
|
||||
* https://petprojects.googlecode.com/svn/trunk/MIT-LICENSE.txt
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(function($) {
|
||||
function make_bold(text) {
|
||||
const idx = text.indexOf(":");
|
||||
if (idx > 0) {
|
||||
return `<b>${text.substring(0, idx)}</b><span class="text-muted">: ${text.substring(idx + 1)}`;
|
||||
} else {
|
||||
return `<b>${text}</b>`;
|
||||
}
|
||||
}
|
||||
|
||||
function hide_error(input) {
|
||||
const err = input.parent().parent().find(".invalid-remaining");
|
||||
err.hide();
|
||||
}
|
||||
|
||||
function show_error(input, msg) {
|
||||
const err = input.parent().parent().find(".invalid-remaining");
|
||||
err.text(msg);
|
||||
err.show();
|
||||
}
|
||||
|
||||
$.fn.selectSelector = function(source, select) {
|
||||
return this.each(function() {
|
||||
const selector = $(this),
|
||||
input = $('input[type=text]', this);
|
||||
|
||||
const lookup = {};
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
lookup[source[i].id] = source[i];
|
||||
}
|
||||
|
||||
selector.click(() => input.focus())
|
||||
.delegate('.badge a', 'click', function() {
|
||||
const id = $(this).parent().data("id");
|
||||
select.find("option[value=" + id + "]").attr("selected", false)
|
||||
recreate();
|
||||
});
|
||||
|
||||
function addTag(item) {
|
||||
const id = item.id;
|
||||
|
||||
let text = item.text;
|
||||
const idx = text.indexOf(':');
|
||||
if (idx > 0) {
|
||||
text = text.substr(0, idx);
|
||||
}
|
||||
|
||||
$('<span class="badge roaded-pill bg-primary"/>')
|
||||
.text(text + ' ')
|
||||
.data("id", id)
|
||||
.append('<a>x</a>')
|
||||
.insertBefore(input);
|
||||
input.attr("placeholder", null);
|
||||
select.find("option[value='" + id + "']").attr("selected", "selected")
|
||||
hide_error(input);
|
||||
}
|
||||
|
||||
function recreate() {
|
||||
selector.find("span").remove();
|
||||
select.find("option").each(function() {
|
||||
if (this.hasAttribute("selected")) {
|
||||
addTag(lookup[this.getAttribute("value")]);
|
||||
}
|
||||
});
|
||||
}
|
||||
recreate();
|
||||
|
||||
input.focusout(function() {
|
||||
const value = input.val().trim();
|
||||
if (value !== "") {
|
||||
show_error(input, "Please select an existing tag, it's not possible to add custom ones.");
|
||||
}
|
||||
})
|
||||
|
||||
input.keydown(function(e) {
|
||||
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}).autocomplete({
|
||||
minLength: 0,
|
||||
source: source,
|
||||
select: function(event, ui) {
|
||||
addTag(ui.item);
|
||||
input.val("");
|
||||
return false;
|
||||
}
|
||||
}).focus(function() {
|
||||
$(this).data("ui-autocomplete").search($(this).val());
|
||||
});
|
||||
|
||||
input.data('ui-autocomplete')._renderItem = function(ul, item) {
|
||||
return $('<li/>')
|
||||
.data('item.autocomplete', item)
|
||||
.append($('<a/>').html(item.toString()))
|
||||
.appendTo(ul);
|
||||
};
|
||||
|
||||
input.data('ui-autocomplete')._resizeMenu = function() {
|
||||
const ul = this.menu.element;
|
||||
ul.outerWidth(Math.max(
|
||||
ul.width('').outerWidth(),
|
||||
selector.outerWidth()
|
||||
));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$(".multichoice_selector").each(function() {
|
||||
const ele = $(this);
|
||||
const sel = ele.parent().find("select");
|
||||
sel.hide();
|
||||
|
||||
const options = [];
|
||||
sel.find("option").each(function() {
|
||||
const text = $(this).text();
|
||||
const option = {
|
||||
id: $(this).attr("value"),
|
||||
text: text,
|
||||
selected: !!$(this).attr("selected"),
|
||||
toString: function() { return make_bold(text); },
|
||||
};
|
||||
|
||||
const idx = text.indexOf(":");
|
||||
if (idx > 0) {
|
||||
option.title = text.substring(0, idx);
|
||||
option.description = text.substring(idx + 1);
|
||||
} else {
|
||||
option.title = text
|
||||
}
|
||||
|
||||
options.push(option);
|
||||
});
|
||||
|
||||
ele.selectSelector(options, sel);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
33
app/public/static/js/topic_discard.js
Normal file
33
app/public/static/js/topic_discard.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
document.querySelectorAll(".topic-discard").forEach(ele => ele.addEventListener("click", (e) => {
|
||||
const row = ele.parentNode.parentNode;
|
||||
const tid = ele.getAttribute("data-tid");
|
||||
const discard = !row.classList.contains("discardtopic");
|
||||
fetch(new Request("/api/topic_discard/?tid=" + tid +
|
||||
"&discard=" + (discard ? "true" : "false"), {
|
||||
method: "post",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"X-CSRFToken": csrf_token,
|
||||
},
|
||||
})).then(function(response) {
|
||||
response.text().then(function(txt) {
|
||||
if (JSON.parse(txt).discarded) {
|
||||
row.classList.add("discardtopic");
|
||||
ele.classList.remove("btn-danger");
|
||||
ele.classList.add("btn-success");
|
||||
ele.innerText = "Show";
|
||||
} else {
|
||||
row.classList.remove("discardtopic");
|
||||
ele.classList.remove("btn-success");
|
||||
ele.classList.add("btn-danger");
|
||||
ele.innerText = "Discard";
|
||||
}
|
||||
}).catch(console.error);
|
||||
}).catch(console.error);
|
||||
}));
|
||||
39
app/public/static/js/video_embed.js
Normal file
39
app/public/static/js/video_embed.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
|
||||
"use strict";
|
||||
|
||||
document.querySelectorAll(".video-embed").forEach(ele => {
|
||||
try {
|
||||
const href = ele.getAttribute("href");
|
||||
const url = new URL(href);
|
||||
|
||||
if (url.host == "www.youtube.com") {
|
||||
ele.addEventListener("click", () => {
|
||||
ele.parentNode.classList.add("d-block");
|
||||
ele.classList.add("ratio");
|
||||
ele.classList.add("ratio-16x9");
|
||||
ele.innerHTML = `
|
||||
<iframe title="YouTube video player" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>`;
|
||||
|
||||
const embedURL = new URL("https://www.youtube.com/");
|
||||
embedURL.pathname = "/embed/" + url.searchParams.get("v");
|
||||
embedURL.searchParams.set("autoplay", "1");
|
||||
|
||||
const iframe = ele.children[0];
|
||||
iframe.setAttribute("src", embedURL);
|
||||
});
|
||||
|
||||
ele.setAttribute("data-src", href);
|
||||
ele.removeAttribute("href");
|
||||
|
||||
ele.querySelector(".label").innerText = "YouTube";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(url);
|
||||
return;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user