mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-04-19 21:42:24 +00:00

Added an option to use the server's public IPv4 when generating QR codes and URIs. Included necessary translation updates for the new option.
299 lines
No EOL
9.2 KiB
HTML
299 lines
No EOL
9.2 KiB
HTML
{{define "modals/qrcodeModal"}}
|
|
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
|
:dialog-style="isMobile ? { top: '18px' } : {}" :closable="true" :class="themeSwitcher.currentTheme" :footer="null"
|
|
width="fit-content">
|
|
<tr-qr-modal class="qr-modal">
|
|
|
|
<template v-if="app.subSettings.enable && qrModal.subId">
|
|
<tr-qr-box class="qr-box">
|
|
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
|
|
<tr-qr-bg class="qr-bg-sub">
|
|
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
|
<canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
|
|
</tr-qr-bg-inner>
|
|
</tr-qr-bg>
|
|
</tr-qr-box>
|
|
<tr-qr-box class="qr-box">
|
|
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
|
<tr-qr-bg class="qr-bg-sub">
|
|
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
|
<canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
|
|
</tr-qr-bg-inner>
|
|
</tr-qr-bg>
|
|
</tr-qr-box>
|
|
</template>
|
|
<template v-for="(row, index) in qrModal.qrcodes">
|
|
<tr-qr-box class="qr-box">
|
|
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
|
<span>{{ i18n "useIPv4ForHost" }}: </span>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
:aria-checked="row.useIPv4"
|
|
:class="['ant-switch', 'ant-switch-small', { 'ant-switch-checked': row.useIPv4 }]"
|
|
@click="toggleIPv4(index)"
|
|
style="margin-left: 8px;">
|
|
<span class="ant-switch-inner"></span>
|
|
</button>
|
|
</div>
|
|
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
|
|
<tr-qr-bg class="qr-bg">
|
|
<canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
|
|
</tr-qr-bg>
|
|
</tr-qr-box>
|
|
</template>
|
|
</tr-qr-modal>
|
|
</a-modal>
|
|
|
|
<style>
|
|
.ant-table:not(.ant-table-expanded-row .ant-table) {
|
|
outline: 1px solid #f0f0f0;
|
|
outline-offset: -1px;
|
|
border-radius: 1rem;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* QR code transition effects */
|
|
.qr-cv {
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
|
|
.qr-transition-enter-active, .qr-transition-leave-active {
|
|
transition: opacity 0.3s, transform 0.3s;
|
|
}
|
|
|
|
.qr-transition-enter, .qr-transition-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.9);
|
|
}
|
|
|
|
.qr-transition-enter-to, .qr-transition-leave {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.qr-flash {
|
|
animation: qr-flash-animation 0.6s;
|
|
}
|
|
|
|
@keyframes qr-flash-animation {
|
|
0% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
transform: scale(0.95);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
const qrModal = {
|
|
title: '',
|
|
dbInbound: new DBInbound(),
|
|
client: null,
|
|
qrcodes: [],
|
|
visible: false,
|
|
subId: '',
|
|
show: function (title = '', dbInbound, client) {
|
|
this.title = title;
|
|
this.dbInbound = dbInbound;
|
|
this.inbound = dbInbound.toInbound();
|
|
this.client = client;
|
|
this.subId = '';
|
|
this.qrcodes = [];
|
|
// Reset the status fetched flag when showing the modal
|
|
if (qrModalApp) qrModalApp.statusFetched = false;
|
|
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
|
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
|
|
this.qrcodes.push({
|
|
remark: "Peer " + (index + 1),
|
|
link: l,
|
|
useIPv4: false,
|
|
originalLink: l
|
|
});
|
|
});
|
|
} else {
|
|
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
|
this.qrcodes.push({
|
|
remark: l.remark,
|
|
link: l.link,
|
|
useIPv4: false,
|
|
originalLink: l.link
|
|
});
|
|
});
|
|
}
|
|
this.visible = true;
|
|
},
|
|
close: function () {
|
|
this.visible = false;
|
|
},
|
|
};
|
|
const qrModalApp = new Vue({
|
|
delimiters: ['[[', ']]'],
|
|
el: '#qrcode-modal',
|
|
mixins: [MediaQueryMixin],
|
|
data: {
|
|
qrModal: qrModal,
|
|
serverStatus: null,
|
|
statusFetched: false,
|
|
},
|
|
methods: {
|
|
async getStatus() {
|
|
try {
|
|
const msg = await HttpUtil.post('/server/status');
|
|
if (msg.success) {
|
|
this.serverStatus = msg.obj;
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to get status:", e);
|
|
}
|
|
},
|
|
|
|
toggleIPv4(index) {
|
|
const row = qrModal.qrcodes[index];
|
|
row.useIPv4 = !row.useIPv4;
|
|
this.updateLink(index);
|
|
},
|
|
updateLink(index) {
|
|
const row = qrModal.qrcodes[index];
|
|
if (!this.serverStatus || !this.serverStatus.publicIP) {
|
|
return;
|
|
}
|
|
|
|
if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
|
|
// Replace the hostname or IP in the link with the IPv4 address
|
|
const originalLink = row.originalLink;
|
|
const url = new URL(originalLink);
|
|
const ipv4 = this.serverStatus.publicIP.ipv4;
|
|
|
|
if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
|
|
// Special handling for WireGuard config
|
|
const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
|
|
const match = originalLink.match(endpointRegex);
|
|
if (match) {
|
|
row.link = originalLink.replace(
|
|
`Endpoint = ${match[1]}:${match[2]}`,
|
|
`Endpoint = ${ipv4}:${match[2]}`
|
|
);
|
|
}
|
|
} else {
|
|
// For other protocols using URL format
|
|
url.hostname = ipv4;
|
|
row.link = url.toString();
|
|
}
|
|
} else {
|
|
// Restore original link
|
|
row.link = row.originalLink;
|
|
}
|
|
|
|
// Update QR code with transition effect
|
|
const canvasElement = document.querySelector('#qrCode-' + index);
|
|
if (canvasElement) {
|
|
// Add flash animation class
|
|
canvasElement.classList.add('qr-flash');
|
|
|
|
// Remove the class after animation completes
|
|
setTimeout(() => {
|
|
canvasElement.classList.remove('qr-flash');
|
|
}, 600);
|
|
}
|
|
|
|
this.setQrCode("qrCode-" + index, row.link);
|
|
},
|
|
copy(content) {
|
|
ClipboardManager
|
|
.copyText(content)
|
|
.then(() => {
|
|
app.$message.success('{{ i18n "copied" }}')
|
|
})
|
|
},
|
|
setQrCode(elementId, content) {
|
|
new QRious({
|
|
element: document.querySelector('#' + elementId),
|
|
size: 400,
|
|
value: content,
|
|
background: 'white',
|
|
backgroundAlpha: 0,
|
|
foreground: 'black',
|
|
padding: 2,
|
|
level: 'L'
|
|
});
|
|
},
|
|
genSubLink(subID) {
|
|
return app.subSettings.subURI + subID;
|
|
},
|
|
genSubJsonLink(subID) {
|
|
return app.subSettings.subJsonURI + subID;
|
|
},
|
|
revertOverflow() {
|
|
const elements = document.querySelectorAll(".qr-tag");
|
|
elements.forEach((element) => {
|
|
element.classList.remove("tr-marquee");
|
|
element.children[0].style.animation = '';
|
|
while (element.children.length > 1) {
|
|
element.removeChild(element.lastChild);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
updated() {
|
|
if (this.qrModal.visible) {
|
|
fixOverflow();
|
|
if (!this.statusFetched) {
|
|
this.getStatus();
|
|
this.statusFetched = true;
|
|
}
|
|
} else {
|
|
this.revertOverflow();
|
|
// Reset the flag when modal is closed so it will fetch again next time
|
|
this.statusFetched = false;
|
|
}
|
|
if (qrModal.client && qrModal.client.subId) {
|
|
qrModal.subId = qrModal.client.subId;
|
|
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
|
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
|
}
|
|
qrModal.qrcodes.forEach((element, index) => {
|
|
this.setQrCode("qrCode-" + index, element.link);
|
|
// Update links based on current toggle state
|
|
if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {
|
|
this.updateLink(index);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
function fixOverflow() {
|
|
const elements = document.querySelectorAll(".qr-tag");
|
|
elements.forEach((element) => {
|
|
function isElementOverflowing(element) {
|
|
const overflowX = element.offsetWidth < element.scrollWidth,
|
|
overflowY = element.offsetHeight < element.scrollHeight;
|
|
return overflowX || overflowY;
|
|
}
|
|
|
|
function wrapContentsInMarquee(element) {
|
|
element.classList.add("tr-marquee");
|
|
element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5
|
|
}s ease-in-out infinite`;
|
|
const marqueeText = element.children[0];
|
|
if (element.children.length < 2) {
|
|
for (let i = 0; i < 1; i++) {
|
|
const marqueeText = element.children[0].cloneNode(true);
|
|
element.children[0].after(marqueeText);
|
|
}
|
|
}
|
|
}
|
|
if (isElementOverflowing(element)) {
|
|
wrapContentsInMarquee(element);
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{{end}} |