|  | @@ -1,10 +1,9 @@
 | 
	
		
			
				|  |  |  {{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">
 | 
	
		
			
				|  |  | +  :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>
 | 
	
	
		
			
				|  | @@ -25,6 +24,18 @@
 | 
	
		
			
				|  |  |      </template>
 | 
	
		
			
				|  |  |      <template v-for="(row, index) in qrModal.qrcodes">
 | 
	
		
			
				|  |  |        <tr-qr-box class="qr-box">
 | 
	
		
			
				|  |  | +        <div :style="{ display: 'flex', alignItems: 'center', marginBottom: '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="{ marginLeft: '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>
 | 
	
	
		
			
				|  | @@ -34,6 +45,53 @@
 | 
	
		
			
				|  |  |    </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: '',
 | 
	
	
		
			
				|  | @@ -42,31 +100,37 @@
 | 
	
		
			
				|  |  |      qrcodes: [],
 | 
	
		
			
				|  |  |      visible: false,
 | 
	
		
			
				|  |  |      subId: '',
 | 
	
		
			
				|  |  | -    show: function(title = '', dbInbound, client) {
 | 
	
		
			
				|  |  | +    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
 | 
	
		
			
				|  |  | +            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
 | 
	
		
			
				|  |  | +            link: l.link,
 | 
	
		
			
				|  |  | +            useIPv4: false,
 | 
	
		
			
				|  |  | +            originalLink: l.link
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        this.visible = true;
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  | -    close: function() {
 | 
	
		
			
				|  |  | +    close: function () {
 | 
	
		
			
				|  |  |        this.visible = false;
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |    };
 | 
	
	
		
			
				|  | @@ -76,8 +140,72 @@
 | 
	
		
			
				|  |  |      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)
 | 
	
	
		
			
				|  | @@ -117,8 +245,14 @@
 | 
	
		
			
				|  |  |      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;
 | 
	
	
		
			
				|  | @@ -127,6 +261,10 @@
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        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);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    });
 | 
	
	
		
			
				|  | @@ -142,8 +280,7 @@
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        function wrapContentsInMarquee(element) {
 | 
	
		
			
				|  |  |          element.classList.add("tr-marquee");
 | 
	
		
			
				|  |  | -        element.children[0].style.animation = `move-ltr ${
 | 
	
		
			
				|  |  | -            (element.children[0].clientWidth / element.clientWidth) * 5
 | 
	
		
			
				|  |  | +        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) {
 | 
	
	
		
			
				|  | @@ -159,4 +296,4 @@
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  </script>
 | 
	
		
			
				|  |  | -{{end}}
 | 
	
		
			
				|  |  | +{{end}}
 |