qrcode_modal.html 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. {{define "modals/qrcodeModal"}}
  2. <a-modal id="qrcode-modal" v-model="qrModal.visible" :closable="true" :class="themeSwitcher.currentTheme"
  3. width="fit-content" :dialog-style="isMobile ? { top: '18px' } : {}" :footer="null">
  4. <template #title>
  5. <a-space direction="horizontal">
  6. <span>[[ qrModal.title ]]</span>
  7. <a-popover :overlay-class-name="themeSwitcher.currentTheme" trigger="click" placement="bottom">
  8. <template slot="content">
  9. <a-space direction="vertical">
  10. <template v-for="(row, index) in qrModal.qrcodes">
  11. <b>[[ row.remark ]]</b>
  12. <a-space direction="horizontal">
  13. <a-switch size="small" :checked="row.useIPv4" @click="toggleIPv4(index)"></a-switch>
  14. <span>{{ i18n "useIPv4ForHost" }}</span>
  15. </a-space>
  16. </template>
  17. </a-space>
  18. </template>
  19. <a-icon type="setting"></a-icon>
  20. </a-popover>
  21. </a-space>
  22. </template>
  23. <tr-qr-modal class="qr-modal">
  24. <template v-if="app.subSettings.enable && qrModal.subId">
  25. <tr-qr-box class="qr-box">
  26. <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
  27. <tr-qr-bg class="qr-bg-sub">
  28. <tr-qr-bg-inner class="qr-bg-sub-inner">
  29. <canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
  30. </tr-qr-bg-inner>
  31. </tr-qr-bg>
  32. </tr-qr-box>
  33. <tr-qr-box class="qr-box">
  34. <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
  35. <tr-qr-bg class="qr-bg-sub">
  36. <tr-qr-bg-inner class="qr-bg-sub-inner">
  37. <canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
  38. </tr-qr-bg-inner>
  39. </tr-qr-bg>
  40. </tr-qr-box>
  41. </template>
  42. <template v-for="(row, index) in qrModal.qrcodes">
  43. <tr-qr-box class="qr-box">
  44. <a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
  45. <tr-qr-bg class="qr-bg">
  46. <canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
  47. </tr-qr-bg>
  48. </tr-qr-box>
  49. </template>
  50. </tr-qr-modal>
  51. </a-modal>
  52. <style>
  53. .ant-table:not(.ant-table-expanded-row .ant-table) {
  54. outline: 1px solid #f0f0f0;
  55. outline-offset: -1px;
  56. border-radius: 1rem;
  57. overflow-x: hidden;
  58. }
  59. /* QR code transition effects */
  60. .qr-cv {
  61. transition: all 0.3s ease-in-out;
  62. }
  63. .qr-transition-enter-active, .qr-transition-leave-active {
  64. transition: opacity 0.3s, transform 0.3s;
  65. }
  66. .qr-transition-enter, .qr-transition-leave-to {
  67. opacity: 0;
  68. transform: scale(0.9);
  69. }
  70. .qr-transition-enter-to, .qr-transition-leave {
  71. opacity: 1;
  72. transform: scale(1);
  73. }
  74. .qr-flash {
  75. animation: qr-flash-animation 0.6s;
  76. }
  77. @keyframes qr-flash-animation {
  78. 0% {
  79. opacity: 1;
  80. transform: scale(1);
  81. }
  82. 50% {
  83. opacity: 0.5;
  84. transform: scale(0.95);
  85. }
  86. 100% {
  87. opacity: 1;
  88. transform: scale(1);
  89. }
  90. }
  91. </style>
  92. <script>
  93. const qrModal = {
  94. title: '',
  95. dbInbound: new DBInbound(),
  96. client: null,
  97. qrcodes: [],
  98. visible: false,
  99. subId: '',
  100. show: function (title = '', dbInbound, client) {
  101. this.title = title;
  102. this.dbInbound = dbInbound;
  103. this.inbound = dbInbound.toInbound();
  104. this.client = client;
  105. this.subId = '';
  106. this.qrcodes = [];
  107. // Reset the status fetched flag when showing the modal
  108. if (qrModalApp) qrModalApp.statusFetched = false;
  109. if (this.inbound.protocol == Protocols.WIREGUARD) {
  110. this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
  111. this.qrcodes.push({
  112. remark: "Peer " + (index + 1),
  113. link: l,
  114. useIPv4: false,
  115. originalLink: l
  116. });
  117. });
  118. } else {
  119. this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
  120. this.qrcodes.push({
  121. remark: l.remark,
  122. link: l.link,
  123. useIPv4: false,
  124. originalLink: l.link
  125. });
  126. });
  127. }
  128. this.visible = true;
  129. },
  130. close: function () {
  131. this.visible = false;
  132. },
  133. };
  134. const qrModalApp = new Vue({
  135. delimiters: ['[[', ']]'],
  136. el: '#qrcode-modal',
  137. mixins: [MediaQueryMixin],
  138. data: {
  139. qrModal: qrModal,
  140. serverStatus: null,
  141. statusFetched: false,
  142. },
  143. methods: {
  144. async getStatus() {
  145. try {
  146. const msg = await HttpUtil.post('/server/status');
  147. if (msg.success) {
  148. this.serverStatus = msg.obj;
  149. }
  150. } catch (e) {
  151. console.error("Failed to get status:", e);
  152. }
  153. },
  154. toggleIPv4(index) {
  155. const row = qrModal.qrcodes[index];
  156. row.useIPv4 = !row.useIPv4;
  157. this.updateLink(index);
  158. },
  159. updateLink(index) {
  160. const row = qrModal.qrcodes[index];
  161. if (!this.serverStatus || !this.serverStatus.publicIP) {
  162. return;
  163. }
  164. if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
  165. // Replace the hostname or IP in the link with the IPv4 address
  166. const originalLink = row.originalLink;
  167. const url = new URL(originalLink);
  168. const ipv4 = this.serverStatus.publicIP.ipv4;
  169. if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
  170. // Special handling for WireGuard config
  171. const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
  172. const match = originalLink.match(endpointRegex);
  173. if (match) {
  174. row.link = originalLink.replace(
  175. `Endpoint = ${match[1]}:${match[2]}`,
  176. `Endpoint = ${ipv4}:${match[2]}`
  177. );
  178. }
  179. } else {
  180. // For other protocols using URL format
  181. url.hostname = ipv4;
  182. row.link = url.toString();
  183. }
  184. } else {
  185. // Restore original link
  186. row.link = row.originalLink;
  187. }
  188. // Update QR code with transition effect
  189. const canvasElement = document.querySelector('#qrCode-' + index);
  190. if (canvasElement) {
  191. // Add flash animation class
  192. canvasElement.classList.add('qr-flash');
  193. // Remove the class after animation completes
  194. setTimeout(() => {
  195. canvasElement.classList.remove('qr-flash');
  196. }, 600);
  197. }
  198. this.setQrCode("qrCode-" + index, row.link);
  199. },
  200. copy(content) {
  201. ClipboardManager
  202. .copyText(content)
  203. .then(() => {
  204. app.$message.success('{{ i18n "copied" }}')
  205. })
  206. },
  207. setQrCode(elementId, content) {
  208. new QRious({
  209. element: document.querySelector('#' + elementId),
  210. size: 400,
  211. value: content,
  212. background: 'white',
  213. backgroundAlpha: 0,
  214. foreground: 'black',
  215. padding: 2,
  216. level: 'L'
  217. });
  218. },
  219. genSubLink(subID) {
  220. return app.subSettings.subURI + subID;
  221. },
  222. genSubJsonLink(subID) {
  223. return app.subSettings.subJsonURI + subID;
  224. },
  225. revertOverflow() {
  226. const elements = document.querySelectorAll(".qr-tag");
  227. elements.forEach((element) => {
  228. element.classList.remove("tr-marquee");
  229. element.children[0].style.animation = '';
  230. while (element.children.length > 1) {
  231. element.removeChild(element.lastChild);
  232. }
  233. });
  234. }
  235. },
  236. updated() {
  237. if (this.qrModal.visible) {
  238. fixOverflow();
  239. if (!this.statusFetched) {
  240. this.getStatus();
  241. this.statusFetched = true;
  242. }
  243. } else {
  244. this.revertOverflow();
  245. // Reset the flag when modal is closed so it will fetch again next time
  246. this.statusFetched = false;
  247. }
  248. if (qrModal.client && qrModal.client.subId) {
  249. qrModal.subId = qrModal.client.subId;
  250. this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
  251. this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
  252. }
  253. qrModal.qrcodes.forEach((element, index) => {
  254. this.setQrCode("qrCode-" + index, element.link);
  255. // Update links based on current toggle state
  256. if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {
  257. this.updateLink(index);
  258. }
  259. });
  260. }
  261. });
  262. function fixOverflow() {
  263. const elements = document.querySelectorAll(".qr-tag");
  264. elements.forEach((element) => {
  265. function isElementOverflowing(element) {
  266. const overflowX = element.offsetWidth < element.scrollWidth,
  267. overflowY = element.offsetHeight < element.scrollHeight;
  268. return overflowX || overflowY;
  269. }
  270. function wrapContentsInMarquee(element) {
  271. element.classList.add("tr-marquee");
  272. element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5
  273. }s ease-in-out infinite`;
  274. const marqueeText = element.children[0];
  275. if (element.children.length < 2) {
  276. for (let i = 0; i < 1; i++) {
  277. const marqueeText = element.children[0].cloneNode(true);
  278. element.children[0].after(marqueeText);
  279. }
  280. }
  281. }
  282. if (isElementOverflowing(element)) {
  283. wrapContentsInMarquee(element);
  284. }
  285. });
  286. }
  287. </script>
  288. {{end}}