qrcode_modal.html 9.2 KB

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