2025-03-24 09:57:37 +00:00
|
|
|
{{define "modals/qrcodeModal"}}
|
2025-04-19 15:36:17 +00:00
|
|
|
<a-modal id="qrcode-modal" v-model="qrModal.visible" :closable="true" :class="themeSwitcher.currentTheme"
|
|
|
|
width="fit-content" :dialog-style="isMobile ? { top: '18px' } : {}" :footer="null">
|
2024-04-20 18:45:36 +00:00
|
|
|
<tr-qr-modal class="qr-modal">
|
2025-04-19 15:36:17 +00:00
|
|
|
<template #title>
|
|
|
|
<a-space direction="horizontal">
|
|
|
|
<span>[[ qrModal.title ]]</span>
|
|
|
|
<a-popover :overlay-class-name="themeSwitcher.currentTheme" trigger="click" placement="bottom">
|
|
|
|
<template slot="content">
|
|
|
|
<a-space direction="vertical">
|
|
|
|
<template v-for="(row, index) in qrModal.qrcodes">
|
|
|
|
<b>[[ row.remark ]]</b>
|
|
|
|
<a-space direction="horizontal">
|
|
|
|
<a-switch size="small" :checked="row.useIPv4" @click="toggleIPv4(index)"></a-switch>
|
|
|
|
<span>{{ i18n "useIPv4ForHost" }}</span>
|
|
|
|
</a-space>
|
|
|
|
</template>
|
|
|
|
</a-space>
|
|
|
|
</template>
|
|
|
|
<a-icon type="setting"></a-icon>
|
|
|
|
</a-popover>
|
|
|
|
</a-space>
|
|
|
|
</template>
|
2024-04-20 18:45:36 +00:00
|
|
|
<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">
|
2025-03-08 15:41:27 +00:00
|
|
|
<canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
|
2024-04-20 18:45:36 +00:00
|
|
|
</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">
|
2025-03-08 15:41:27 +00:00
|
|
|
<canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
|
2024-04-20 18:45:36 +00:00
|
|
|
</tr-qr-bg-inner>
|
|
|
|
</tr-qr-bg>
|
|
|
|
</tr-qr-box>
|
|
|
|
</template>
|
2025-02-04 10:27:58 +00:00
|
|
|
<template v-for="(row, index) in qrModal.qrcodes">
|
|
|
|
<tr-qr-box class="qr-box">
|
|
|
|
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
|
|
|
|
<tr-qr-bg class="qr-bg">
|
2025-03-08 15:41:27 +00:00
|
|
|
<canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
|
2025-02-04 10:27:58 +00:00
|
|
|
</tr-qr-bg>
|
|
|
|
</tr-qr-box>
|
2024-04-20 18:45:36 +00:00
|
|
|
</template>
|
|
|
|
</tr-qr-modal>
|
2023-02-09 19:18:06 +00:00
|
|
|
</a-modal>
|
|
|
|
|
2025-04-19 15:32:22 +00:00
|
|
|
<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>
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
<script>
|
2024-04-20 18:45:36 +00:00
|
|
|
const qrModal = {
|
|
|
|
title: '',
|
|
|
|
dbInbound: new DBInbound(),
|
|
|
|
client: null,
|
|
|
|
qrcodes: [],
|
|
|
|
visible: false,
|
|
|
|
subId: '',
|
2025-04-19 15:32:22 +00:00
|
|
|
show: function (title = '', dbInbound, client) {
|
2024-04-20 18:45:36 +00:00
|
|
|
this.title = title;
|
|
|
|
this.dbInbound = dbInbound;
|
|
|
|
this.inbound = dbInbound.toInbound();
|
|
|
|
this.client = client;
|
|
|
|
this.subId = '';
|
|
|
|
this.qrcodes = [];
|
2025-04-19 15:32:22 +00:00
|
|
|
// Reset the status fetched flag when showing the modal
|
|
|
|
if (qrModalApp) qrModalApp.statusFetched = false;
|
2024-04-20 18:45:36 +00:00
|
|
|
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
|
|
|
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
|
|
|
|
this.qrcodes.push({
|
|
|
|
remark: "Peer " + (index + 1),
|
2025-04-19 15:32:22 +00:00
|
|
|
link: l,
|
|
|
|
useIPv4: false,
|
|
|
|
originalLink: l
|
2024-04-20 18:45:36 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
|
|
|
this.qrcodes.push({
|
|
|
|
remark: l.remark,
|
2025-04-19 15:32:22 +00:00
|
|
|
link: l.link,
|
|
|
|
useIPv4: false,
|
|
|
|
originalLink: l.link
|
2024-04-20 18:45:36 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.visible = true;
|
|
|
|
},
|
2025-04-19 15:32:22 +00:00
|
|
|
close: function () {
|
2024-04-20 18:45:36 +00:00
|
|
|
this.visible = false;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const qrModalApp = new Vue({
|
|
|
|
delimiters: ['[[', ']]'],
|
|
|
|
el: '#qrcode-modal',
|
2025-04-06 09:40:33 +00:00
|
|
|
mixins: [MediaQueryMixin],
|
2024-04-20 18:45:36 +00:00
|
|
|
data: {
|
2025-02-04 10:27:58 +00:00
|
|
|
qrModal: qrModal,
|
2025-04-19 15:32:22 +00:00
|
|
|
serverStatus: null,
|
|
|
|
statusFetched: false,
|
2024-04-20 18:45:36 +00:00
|
|
|
},
|
|
|
|
methods: {
|
2025-04-19 15:32:22 +00:00
|
|
|
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);
|
|
|
|
},
|
2025-03-07 07:27:33 +00:00
|
|
|
copy(content) {
|
|
|
|
ClipboardManager
|
|
|
|
.copyText(content)
|
|
|
|
.then(() => {
|
|
|
|
app.$message.success('{{ i18n "copied" }}')
|
|
|
|
})
|
2024-04-20 18:45:36 +00:00
|
|
|
},
|
2024-07-07 09:55:59 +00:00
|
|
|
setQrCode(elementId, content) {
|
2024-04-20 18:45:36 +00:00
|
|
|
new QRious({
|
2024-07-07 09:55:59 +00:00
|
|
|
element: document.querySelector('#' + elementId),
|
2024-04-20 18:45:36 +00:00
|
|
|
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();
|
2025-04-19 15:32:22 +00:00
|
|
|
if (!this.statusFetched) {
|
|
|
|
this.getStatus();
|
|
|
|
this.statusFetched = true;
|
|
|
|
}
|
2024-04-20 18:45:36 +00:00
|
|
|
} else {
|
|
|
|
this.revertOverflow();
|
2025-04-19 15:32:22 +00:00
|
|
|
// Reset the flag when modal is closed so it will fetch again next time
|
|
|
|
this.statusFetched = false;
|
2024-04-20 18:45:36 +00:00
|
|
|
}
|
|
|
|
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);
|
2025-04-19 15:32:22 +00:00
|
|
|
// Update links based on current toggle state
|
|
|
|
if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {
|
|
|
|
this.updateLink(index);
|
|
|
|
}
|
2024-04-20 18:45:36 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2023-02-09 19:18:06 +00:00
|
|
|
|
2024-04-20 18:45:36 +00:00
|
|
|
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;
|
|
|
|
}
|
2023-02-09 19:18:06 +00:00
|
|
|
|
2024-04-20 18:45:36 +00:00
|
|
|
function wrapContentsInMarquee(element) {
|
|
|
|
element.classList.add("tr-marquee");
|
2025-04-19 15:32:22 +00:00
|
|
|
element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5
|
2024-04-20 18:45:36 +00:00
|
|
|
}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);
|
|
|
|
}
|
2023-05-22 14:01:41 +00:00
|
|
|
}
|
2024-04-20 18:45:36 +00:00
|
|
|
}
|
|
|
|
if (isElementOverflowing(element)) {
|
|
|
|
wrapContentsInMarquee(element);
|
|
|
|
}
|
2023-02-09 19:18:06 +00:00
|
|
|
});
|
2024-04-20 18:45:36 +00:00
|
|
|
}
|
2023-02-09 19:18:06 +00:00
|
|
|
</script>
|
2025-04-19 15:32:22 +00:00
|
|
|
{{end}}
|