mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-28 06:41:43 +00:00
add key-import button to interface
This commit is contained in:
@ -26,15 +26,18 @@
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
{% if not auth.enabled or auth.username is not none %}
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
|
||||
<button id="package-add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
|
||||
<button id="package-update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
|
||||
<i class="bi bi-play"></i> update
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||
<button id="package-remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
<button id="key-import-btn" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
|
||||
<i class="bi bi-key"></i> import key
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
@ -112,6 +115,7 @@
|
||||
{% include "build-status/success-modal.jinja2" %}
|
||||
|
||||
{% include "build-status/package-add-modal.jinja2" %}
|
||||
{% include "build-status/key-import-modal.jinja2" %}
|
||||
|
||||
{% include "build-status/package-info-modal.jinja2" %}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
<div id="failed-form" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div id="failed-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h4 id="error-title" class="modal-title"></h4>
|
||||
<h4 id="failed-title" class="modal-title"></h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="error-description"></p>
|
||||
<p id="error-details"></p>
|
||||
<p id="failed-description"></p>
|
||||
<p id="failed-details"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
||||
@ -17,16 +17,18 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const failedForm = $("#failed-form");
|
||||
const errorDescription = $("#error-description");
|
||||
const errorDetails = $("#error-details");
|
||||
const errorTitle = $("#error-title");
|
||||
failedForm.on("hidden.bs.modal", () => { reload(); });
|
||||
const failedModal = $("#failed-modal");
|
||||
failedModal.on("hidden.bs.modal", () => { reload(); });
|
||||
|
||||
const failedDescription = $("#failed-description");
|
||||
const failedDetails = $("#failed-details");
|
||||
const failedTitle = $("#failed-title");
|
||||
|
||||
function showFailure(title, description, details) {
|
||||
errorTitle.text(title);
|
||||
errorDescription.text(description);
|
||||
errorDetails.text(details);
|
||||
failedForm.modal("show");
|
||||
failedTitle.text(title);
|
||||
failedDescription.text(description);
|
||||
failedDetails.text(details);
|
||||
|
||||
failedModal.modal("show");
|
||||
}
|
||||
</script>
|
||||
|
@ -0,0 +1,92 @@
|
||||
<div id="key-import-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="key-import-form" onsubmit="return false">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Import key from PGP server</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="key-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="key-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="key-server-input" class="col-sm-2 col-form-label">key server</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="key-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-10">
|
||||
<pre class="language-less"><code id="key-body-input" class="pre-scrollable language-less"></code><button id="key-copy-btn" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" onclick="importPgpKey()"><i class="bi bi-play"></i> import</button>
|
||||
<button type="submit" class="btn btn-success" onclick="fetchPgpKey()"><i class="bi bi-arrow-clockwise"></i> fetch</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const keyImportModal = $("#key-import-modal");
|
||||
const keyImportForm = $("#key-import-form");
|
||||
keyImportModal.on("hidden.bs.modal", () => {
|
||||
keyBodyInput.text("");
|
||||
keyImportForm.trigger("reset");
|
||||
});
|
||||
|
||||
const keyBodyInput = $("#key-body-input");
|
||||
const keyCopyButton = $("#key-copy-btn");
|
||||
|
||||
const keyFingerprintInput = $("#key-fingerprint-input");
|
||||
const keyServerInput = $("#key-server-input");
|
||||
|
||||
async function copyPgpKey() {
|
||||
const logs = keyBodyInput.text();
|
||||
await copyToClipboard(logs, keyCopyButton);
|
||||
}
|
||||
|
||||
function fetchPgpKey() {
|
||||
const key = keyFingerprintInput.val();
|
||||
const server = keyServerInput.val();
|
||||
|
||||
if (key && server) {
|
||||
$.ajax({
|
||||
url: "/api/v1/service/pgp",
|
||||
data: {"key": key, "server": server},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => { keyBodyInput.text(response.key); },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function importPgpKey() {
|
||||
const key = keyFingerprintInput.val();
|
||||
const server = keyServerInput.val();
|
||||
|
||||
if (key && server) {
|
||||
$.ajax({
|
||||
url: "/api/v1/service/pgp",
|
||||
data: JSON.stringify({key: key, server: server}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => {
|
||||
keyImportModal.modal("hide");
|
||||
showSuccess("Success", `Key ${key} has been imported`, "");
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => {
|
||||
showFailure("Action failed", `Could not import key ${key} from ${server}`, errorThrown);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,4 +1,4 @@
|
||||
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div id="login-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form action="/api/v1/login" method="post">
|
||||
@ -19,7 +19,7 @@
|
||||
<div class="input-group">
|
||||
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password" class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password-btn" class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
<script>
|
||||
const passwordInput = $("#password");
|
||||
const showHidePasswordButton = $("#show-hide-password");
|
||||
const showHidePasswordButton = $("#show-hide-password-btn");
|
||||
|
||||
function showPassword() {
|
||||
if (passwordInput.attr("type") === "password") {
|
||||
|
@ -1,61 +1,74 @@
|
||||
<div id="add-form" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add new packages</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="package" class="col-sm-2 col-form-label">package</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="package-form" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
||||
<datalist id="known-packages-dlist"></datalist>
|
||||
<form id="package-add-form" onsubmit="return false">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add new packages</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="package-input" class="col-sm-2 col-form-label">package</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="package-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
||||
<datalist id="known-packages-dlist"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()"><i class="bi bi-play"></i> add</button>
|
||||
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()"><i class="bi bi-plus"></i> request</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
|
||||
<button type="submit" class="btn btn-success" onclick="packagesRequest()"><i class="bi bi-plus"></i> request</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const packageInput = $("#package-form");
|
||||
const knownPackages = $("#known-packages-dlist");
|
||||
const packageAddModal = $("#package-add-modal");
|
||||
const packageAddForm = $("#package-add-form");
|
||||
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
|
||||
|
||||
const packageInput = $("#package-input");
|
||||
const knownPackagesList = $("#known-packages-dlist");
|
||||
packageInput.keyup(() => {
|
||||
clearTimeout(packageInput.data("timeout"));
|
||||
packageInput.data("timeout", setTimeout($.proxy(() => {
|
||||
const value = packageInput.val();
|
||||
|
||||
$.ajax({
|
||||
url: "/api/v1/service/search",
|
||||
data: {"for": value},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const options = response.map(pkg => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pkg.package;
|
||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||
return option;
|
||||
});
|
||||
knownPackages.empty().append(options);
|
||||
},
|
||||
})
|
||||
if (value.length >= 3) {
|
||||
$.ajax({
|
||||
url: "/api/v1/service/search",
|
||||
data: {"for": value},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const options = response.map(pkg => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pkg.package;
|
||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||
return option;
|
||||
});
|
||||
knownPackagesList.empty().append(options);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, this), 500));
|
||||
});
|
||||
|
||||
function addPackages() {
|
||||
const packages = [packageInput.val()];
|
||||
doPackageAction("/api/v1/service/add", packages, "The following package has been added:", "Package addition failed:");
|
||||
function packagesAdd() {
|
||||
const packages = packageInput.val();
|
||||
if (packages) {
|
||||
packageAddModal.modal("hide");
|
||||
doPackageAction("/api/v1/service/add", [packages], "The following package has been added:", "Package addition failed:");
|
||||
}
|
||||
}
|
||||
|
||||
function requestPackages() {
|
||||
const packages = [packageInput.val()];
|
||||
doPackageAction("/api/v1/service/request", packages, "The following package has been requested:", "Package request failed:");
|
||||
function packagesRequest() {
|
||||
const packages = packageInput.val();
|
||||
if (packages) {
|
||||
packageAddModal.modal("hide");
|
||||
doPackageAction("/api/v1/service/request", [packages], "The following package has been requested:", "Package request failed:");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div id="package-info-form" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div id="package-info-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div id="package-info-modal-header" class="modal-header">
|
||||
@ -6,7 +6,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre class="language-logs"><code id="package-info-logs" class="pre-scrollable language-logs"></code><button id="copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||
<pre class="language-logs"><code id="package-info-logs-input" class="pre-scrollable language-logs"></code><button id="logs-copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
|
||||
@ -17,28 +17,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const packageInfoModal = $("#package-info-modal");
|
||||
const packageInfoModalHeader = $("#package-info-modal-header");
|
||||
const packageInfo = $("#package-info");
|
||||
const packageInfoForm = $("#package-info-form");
|
||||
const packageInfoHeader = $("#package-info-modal-header");
|
||||
const packageInfoLogs = $("#package-info-logs");
|
||||
const packageInfoLogsCopyButton = $("#copy-btn");
|
||||
|
||||
const packageInfoLogsInput = $("#package-info-logs-input");
|
||||
const packageInfoLogsCopyButton = $("#logs-copy-btn");
|
||||
|
||||
async function copyLogs() {
|
||||
const logs = packageInfoLogs.text();
|
||||
await copyToClipboard(logs);
|
||||
|
||||
packageInfoLogsCopyButton.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
||||
setTimeout(()=> {
|
||||
packageInfoLogsCopyButton.html("<i class=\"bi bi-clipboard\"></i> copy");
|
||||
}, 2000);
|
||||
const logs = packageInfoLogsInput.text();
|
||||
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
||||
}
|
||||
|
||||
function showLogs(package) {
|
||||
const isPackageBaseSet = package !== undefined;
|
||||
if (isPackageBaseSet)
|
||||
packageInfoForm.data("package", package); // set package base as currently used
|
||||
packageInfoModal.data("package", package); // set package base as currently used
|
||||
else
|
||||
package = packageInfoForm.data("package"); // read package base from the current window attribute
|
||||
package = packageInfoModal.data("package"); // read package base from the current window attribute
|
||||
|
||||
const headerClass = status => {
|
||||
if (status === "pending") return ["bg-warning"];
|
||||
@ -54,13 +50,13 @@
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
||||
packageInfoLogs.text(response.logs);
|
||||
packageInfoLogsInput.text(response.logs);
|
||||
|
||||
packageInfoHeader.removeClass();
|
||||
packageInfoHeader.addClass("modal-header");
|
||||
headerClass(response.status.status).forEach((clz) => packageInfoHeader.addClass(clz));
|
||||
packageInfoModalHeader.removeClass();
|
||||
packageInfoModalHeader.addClass("modal-header");
|
||||
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
|
||||
|
||||
if (isPackageBaseSet) packageInfoForm.modal("show"); // we don't need to show window again
|
||||
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => {
|
||||
// show failed modal in case if first time loading
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div id="success-form" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div id="success-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success text-white">
|
||||
@ -17,16 +17,18 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const successForm = $("#success-form");
|
||||
const successModal = $("#success-modal");
|
||||
successModal.on("hidden.bs.modal", () => { reload(); });
|
||||
|
||||
const successDescription = $("#success-description");
|
||||
const successDetails = $("#success-details");
|
||||
const successTitle = $("#success-title");
|
||||
successForm.on("hidden.bs.modal", () => { reload(); });
|
||||
|
||||
function showSuccess(title, description, details) {
|
||||
successTitle.text(title);
|
||||
successDescription.text(description);
|
||||
successDetails.empty().append(details);
|
||||
successForm.modal("show");
|
||||
|
||||
successModal.modal("show");
|
||||
}
|
||||
</script>
|
||||
|
@ -1,14 +1,19 @@
|
||||
<script>
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
const updateButton = $("#update-btn");
|
||||
const keyImportButton = $("#key-import-btn");
|
||||
const packageAddButton = $("#package-add-btn");
|
||||
const packageRemoveButton = $("#package-remove-btn");
|
||||
const packageUpdateButton = $("#package-update-btn");
|
||||
|
||||
const table = $("#packages");
|
||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
||||
() => {
|
||||
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||
});
|
||||
table.on("click-row.bs.table", (_, row) => { showLogs(row.id); });
|
||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
|
||||
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||
});
|
||||
table.on("click-row.bs.table", (self, data, row, cell) => {
|
||||
if (0 === cell || "base" === cell) {
|
||||
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||
} else showLogs(data.id);
|
||||
});
|
||||
|
||||
const architectureBadge = $("#badge-architecture");
|
||||
const repositoryBadge = $("#badge-repository");
|
||||
@ -50,9 +55,10 @@
|
||||
}
|
||||
|
||||
function hideControls(hidden) {
|
||||
addButton.attr("hidden", hidden);
|
||||
removeButton.attr("hidden", hidden);
|
||||
updateButton.attr("hidden", hidden);
|
||||
keyImportButton.attr("hidden", hidden);
|
||||
packageAddButton.attr("hidden", hidden);
|
||||
packageRemoveButton.attr("hidden", hidden);
|
||||
packageUpdateButton.attr("hidden", hidden);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
<div class="container">
|
||||
{% if pgp_key is not none %}
|
||||
<p>This repository is signed with <a href="https://pgp.mit.edu/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||
<p>This repository is signed with <a href="https://keyserver.ubuntu.com/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>In order to use this repository edit your <code>/etc/pacman.conf</code> as following:</p>
|
||||
@ -101,12 +101,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
|
||||
async function copyPacmanConf() {
|
||||
const conf = pacmanConf.text();
|
||||
await copyToClipboard(conf);
|
||||
|
||||
pacmanConfCopyButton.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
||||
setTimeout(() => {
|
||||
pacmanConfCopyButton.html("<i class=\"bi bi-clipboard\"></i> copy");
|
||||
}, 2000);
|
||||
await copyToClipboard(conf, pacmanConfCopyButton);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -13,16 +13,21 @@
|
||||
<script src="https://unpkg.com/bootstrap-table@1.21.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script>
|
||||
async function copyToClipboard(text) {
|
||||
async function copyToClipboard(text, button) {
|
||||
if (navigator.clipboard === undefined) {
|
||||
const input = document.createElement("textarea");
|
||||
input.innerHTML = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
button.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
||||
setTimeout(()=> {
|
||||
button.html("<i class=\"bi bi-clipboard\"></i> copy");
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user