WME Segment Shift Utility

Utility for shifting street segments in WME without disconnecting nodes

  1. // ==UserScript==
  2. // @name WME Segment Shift Utility
  3. // @namespace https://github.com/kid4rm90s/Segment-Shift-Utility
  4. // @version 2025.07.04.01
  5. // @description Utility for shifting street segments in WME without disconnecting nodes
  6. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/*
  7. // @author kid4rm90s
  8. // @connect gf.qytechs.cn
  9. // @grant GM_xmlhttpRequest
  10. // @grant unsafeWindow
  11. // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
  12. // @require https://cdn.jsdelivr.net/gh/wazeSpace/wme-sdk-plus@06108853094d40f67e923ba0fe0de31b1cec4412/wme-sdk-plus.js
  13. // @exclude https://cdn.jsdelivr.net/gh/WazeSpace/wme-sdk-plus@latest/wme-sdk-plus.js
  14. // @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. /* global getWmeSdk */
  19. /* global initWmeSdkPlus */
  20. /* global WazeWrap */
  21. /* global turf */
  22. /* global $ */
  23. /* global jQuery */
  24. /* global I18n */
  25. /* eslint curly: ["warn", "multi-or-nest"] */
  26.  
  27. /*Scripts modified from WME RA Util (https://gf.qytechs.cn/en/scripts/23616-wme-ra-util)
  28. orgianl author: JustinS83 Waze*/
  29. (function () {
  30. const updateMessage = ' Added keyboard shortcuts (Alt + Arrow keys) for quick segment shifting in all four directions.<br> This improves workflow speed and matches the behavior of other WME utility scripts.';
  31. const SCRIPT_VERSION = GM_info.script.version.toString();
  32. const SCRIPT_NAME = GM_info.script.name;
  33. const DOWNLOAD_URL = GM_info.script.downloadURL;
  34.  
  35. const DIRECTION = {
  36. NORTH: 0,
  37. EAST: 90,
  38. SOUTH: 180,
  39. WEST: 270,
  40. };
  41.  
  42. let sdk;
  43. let _settings;
  44.  
  45. async function bootstrap() {
  46. const wmeSdk = getWmeSdk({ scriptId: 'wme-ss-util', scriptName: 'WME SS Util' });
  47. const sdkPlus = await initWmeSdkPlus(wmeSdk, {
  48. hooks: ['Editing.Transactions'],
  49. });
  50. sdk = sdkPlus || wmeSdk;
  51. sdk.Events.once({ eventName: 'wme-ready' }).then(() => {
  52. loadScriptUpdateMonitor();
  53. init();
  54. });
  55. }
  56.  
  57. function waitForWME() {
  58. if (!unsafeWindow.SDK_INITIALIZED) {
  59. setTimeout(waitForWME, 500);
  60. return;
  61. }
  62. unsafeWindow.SDK_INITIALIZED.then(bootstrap);
  63. }
  64. waitForWME();
  65.  
  66. function loadScriptUpdateMonitor() {
  67. try {
  68. const updateMonitor = new WazeWrap.Alerts.ScriptUpdateMonitor(SCRIPT_NAME, SCRIPT_VERSION, DOWNLOAD_URL, GM_xmlhttpRequest);
  69. updateMonitor.start();
  70. } catch (ex) {
  71. // Report, but don't stop if ScriptUpdateMonitor fails.
  72. console.error(`${SCRIPT_NAME}:`, ex);
  73. }
  74. }
  75.  
  76. function init() {
  77. console.log('SS UTIL', GM_info.script);
  78. injectCss();
  79. // UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry'); // Replaced by SDK
  80. // MoveNode = require("Waze/Action/MoveNode"); // Replaced by SDK
  81. // MultiAction = require("Waze/Action/MultiAction"); // Replaced by SDK
  82.  
  83. SSUtilWindow = document.createElement('div');
  84. SSUtilWindow.id = 'SSUtilWindow'; // Consistent ID
  85. SSUtilWindow.style.position = 'fixed';
  86. SSUtilWindow.style.visibility = 'hidden';
  87. SSUtilWindow.style.top = '15%';
  88. SSUtilWindow.style.left = '25%';
  89. SSUtilWindow.style.width = '250px';
  90. SSUtilWindow.style.zIndex = 100;
  91. SSUtilWindow.style.backgroundColor = '#FFFFFE';
  92. SSUtilWindow.style.borderWidth = '0px';
  93. SSUtilWindow.style.borderStyle = 'solid';
  94. SSUtilWindow.style.borderRadius = '10px';
  95. SSUtilWindow.style.boxShadow = '5px 5px 10px Silver';
  96. SSUtilWindow.style.padding = '4px';
  97.  
  98. let SSUtilWindowHTML =
  99. '<div id="header" style="padding: 4px; background-color:#92C3D3; border-radius: 5px;-moz-border-radius: 5px;-webkit-border-radius: 5px; color: white; font-weight: bold; text-align:center; letter-spacing: 1px;text-shadow: black 0.1em 0.1em 0.2em;"><img src="https://storage.googleapis.com/wazeopedia-files/1/1e/RA_Util.png" style="float:left"></img> Segment Shift Utility <a data-toggle="collapse" href="#divWrappers1" id="collapserLink1" style="float:right"><span id="collapser1" style="cursor:pointer;padding:2px;color:white;" class="fa fa-caret-square-o-up"></a></span></div>';
  100. // start collapse // I put it al the beginning
  101. SSUtilWindowHTML += '<div id="divWrappers1" class="collapse in">';
  102. //***************** Disconnect Nodes Checkbox **************************
  103. SSUtilWindowHTML += '<p style="margin: 10px 0px 0px 20px;"><input type="checkbox" id="chkDisconnectNodes"> Disconnect Nodes</p>';
  104. //***************** Shift Amount **************************
  105. // Define BOX
  106. SSUtilWindowHTML +=
  107. '<div id="contentShift" style="text-align:center;float:left; width: 120px;max-width: 49%;height: 170px;margin: 1em 5px 0px 0px;opacity:1;border-radius: 2px;-moz-border-radius: 2px;-webkit-border-radius: 4px;border-width:1px;border-style:solid;border-color:#92C3D3;padding:2px;}">';
  108. SSUtilWindowHTML +=
  109. '<b>Shift amount</b></br><input type="text" name="shiftAmount" id="shiftAmount" size="1" style="float: left; text-align: center;font: inherit; line-height: normal; width: 30px; height: 20px; margin: 5px 4px; box-sizing: border-box; display: block; padding-left: 0; border-bottom-color: rgba(black,.3); background: transparent; outline: none; color: black;" value="1"/> <div style="margin: 5px 4px;">Metre(s)';
  110. // Shift amount controls
  111. SSUtilWindowHTML +=
  112. '<div id="controls" style="text-align:center; padding:06px 4px;width=100px; height=100px;margin: 5px 0px;border-style:solid; border-width: 2px;border-radius: 50%;-moz-border-radius: 50%;-webkit-border-radius: 50%;box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1);-moz-box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1);-webkit-box-shadow: inset 0px 0px 50px -14px rgba(0,0,0,1); background:#92C3D3;align:center;">';
  113. //Single Shift Up Button
  114. SSUtilWindowHTML += '<span id="SSShiftUpBtn" style="cursor:pointer;font-size:14px;">';
  115. SSUtilWindowHTML += '<i class="fa fa-angle-double-up fa-2x" style="color: white; text-shadow: black 0.1em 0.1em 0.2em; vertical-align: top;"> </i>';
  116. SSUtilWindowHTML += '<span id="UpBtnCaption" style="font-weight: bold;"></span>';
  117. SSUtilWindowHTML += '</span><br>';
  118. //Single Shift Left Button
  119. SSUtilWindowHTML += '<span id="SSShiftLeftBtn" style="cursor:pointer;font-size:14px;margin-left:-40px;">';
  120. SSUtilWindowHTML += '<i class="fa fa-angle-double-left fa-2x" style="color: white; text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  121. SSUtilWindowHTML += '<span id="LeftBtnCaption" style="font-weight: bold;"></span>';
  122. SSUtilWindowHTML += '</span>';
  123. //Single Shift Right Button
  124. SSUtilWindowHTML += '<span id="SSShiftRightBtn" style="float: right;cursor:pointer;font-size:14px;margin-right:5px;">';
  125. SSUtilWindowHTML += '<i class="fa fa-angle-double-right fa-2x" style="color: white;text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  126. SSUtilWindowHTML += '<span id="RightBtnCaption" style="font-weight: bold;"></span>';
  127. SSUtilWindowHTML += '</span><br>';
  128. //Single Shift Down Button
  129. SSUtilWindowHTML += '<span id="SSShiftDownBtn" style="cursor:pointer;font-size:14px;margin-top:0px;">';
  130. SSUtilWindowHTML += '<i class="fa fa-angle-double-down fa-2x" style="color: white;text-shadow: black 0.1em 0.1em 0.2em; vertical-align: middle"> </i>';
  131. SSUtilWindowHTML += '<span id="DownBtnCaption" style="font-weight: bold;"></span>';
  132. SSUtilWindowHTML += '</span>';
  133. SSUtilWindowHTML += '</div></div></div>';
  134.  
  135. SSUtilWindow.innerHTML = SSUtilWindowHTML;
  136. document.body.appendChild(SSUtilWindow);
  137.  
  138. $('#SSShiftLeftBtn').click(SSShiftLeftClick);
  139. $('#SSShiftRightBtn').click(SSShiftRightClick);
  140. $('#SSShiftUpBtn').click(SSShiftUpClick);
  141. $('#SSShiftDownBtn').click(SSShiftDownClick);
  142.  
  143. $('#shiftAmount').keypress(function (event) {
  144. if ((event.which != 46 || $(this).val().indexOf('.') != -1) && (event.which < 48 || event.which > 57)) event.preventDefault();
  145. });
  146.  
  147. // Keyboard shortcut support for direction shift (Alt+Arrow)
  148. document.addEventListener(
  149. 'keydown',
  150. function (e) {
  151. if (!e.altKey) return;
  152. // Prevent triggering when focus is in an input or textarea
  153. const tag = e.target && e.target.tagName ? e.target.tagName.toLowerCase() : '';
  154. if (tag === 'input' || tag === 'textarea') return;
  155. switch (e.key) {
  156. case 'ArrowLeft':
  157. e.preventDefault();
  158. SSShiftLeftClick(e);
  159. break;
  160. case 'ArrowRight':
  161. e.preventDefault();
  162. SSShiftRightClick(e);
  163. break;
  164. case 'ArrowUp':
  165. e.preventDefault();
  166. SSShiftUpClick(e);
  167. break;
  168. case 'ArrowDown':
  169. e.preventDefault();
  170. SSShiftDownClick(e);
  171. break;
  172. }
  173. },
  174. false
  175. );
  176.  
  177. $('#collapserLink1').click(function () {
  178. $('#divWrappers1').slideToggle('fast');
  179. if ($('#collapser1').attr('class') == 'fa fa-caret-square-o-down') {
  180. $('#collapser1').removeClass('fa-caret-square-o-down');
  181. $('#collapser1').addClass('fa-caret-square-o-up');
  182. } else {
  183. $('#collapser1').removeClass('fa-caret-square-o-up');
  184. $('#collapser1').addClass('fa-caret-square-o-down');
  185. }
  186. saveSettingsToStorage();
  187. });
  188.  
  189. const loadedSettings = JSON.parse(localStorage.getItem('WME_SSUtil'));
  190. const defaultSettings = {
  191. divTop: '15%',
  192. divLeft: '25%',
  193. Expanded: true,
  194. DisconnectNodes: false, // default to false (normal behavior)
  195. };
  196. _settings = loadedSettings ?? defaultSettings;
  197.  
  198. $('#SSUtilWindow').css('left', _settings.divLeft);
  199. $('#SSUtilWindow').css('top', _settings.divTop);
  200. $('#chkDisconnectNodes').prop('checked', _settings.DisconnectNodes); // Set checkbox state from settings
  201.  
  202. if (!_settings.Expanded) {
  203. $('#divWrappers1').hide();
  204. $('#collapser1').removeClass('fa-caret-square-o-up');
  205. $('#collapser1').addClass('fa-caret-square-o-down');
  206. }
  207.  
  208. sdk.Events.on({ eventName: 'wme-selection-changed', eventHandler: checkDisplayTool });
  209. WazeWrap.Interface.ShowScriptUpdate('WME SS Util', GM_info.script.version, updateMessage, 'https://update.gf.qytechs.cn/scripts/537258/WME%20Segment%20Shift%20Utility.user.js', 'https://github.com/kid4rm90s/Segment-Shift-Utility');
  210. }
  211.  
  212. function saveSettingsToStorage() {
  213. if (localStorage) {
  214. _settings.divLeft = $('#SSUtilWindow').css('left');
  215. _settings.divTop = $('#SSUtilWindow').css('top');
  216. _settings.Expanded = $('#collapser1').attr('class').indexOf('fa-caret-square-o-up') > -1;
  217. _settings.DisconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Save checkbox state
  218. localStorage.setItem('WME_SSUtil', JSON.stringify(_settings));
  219. }
  220. }
  221.  
  222. function checkDisplayTool() {
  223. if (sdk.Editing.getSelection()?.objectType === 'segment') {
  224. if (sdk.Editing.getSelection().length === 0) {
  225. $('#SSUtilWindow').css({ visibility: 'hidden' });
  226. } else {
  227. $('#SSUtilWindow').css({ visibility: 'visible' });
  228. if (typeof jQuery.ui !== 'undefined') {
  229. $('#SSUtilWindow').draggable({
  230. //Gotta nuke the height setting the dragging inserts otherwise the panel cannot collapse
  231. stop: () => {
  232. $('#SSUtilWindow').css('height', '');
  233. saveSettingsToStorage();
  234. },
  235. });
  236. }
  237. }
  238. } else {
  239. $('#SSUtilWindow').css({ visibility: 'hidden' });
  240. if (typeof jQuery.ui !== 'undefined') {
  241. $('#SSUtilWindow').draggable({
  242. stop: () => {
  243. $('#SSUtilWindow').css('height', '');
  244. saveSettingsToStorage();
  245. },
  246. });
  247. }
  248. }
  249. }
  250.  
  251. function ShiftSegmentNodesLat(offset) {
  252. const selectedSegmentIds = sdk.Editing.getSelection()?.ids;
  253. if (!selectedSegmentIds || selectedSegmentIds.length === 0) {
  254. return;
  255. }
  256.  
  257. const numOffset = parseFloat(offset);
  258. if (isNaN(numOffset)) {
  259. console.error('SS UTIL: Invalid shift amount for Latitude.');
  260. return;
  261. }
  262.  
  263. const disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state
  264.  
  265. sdk.Editing.doActions(() => {
  266. const uniqueNodeIds = new Set();
  267.  
  268. if (!disconnectNodes) {
  269. // Collect unique nodes from selected segments
  270. for (const segmentId of selectedSegmentIds) {
  271. const currentSegment = sdk.DataModel.Segments.getById({ segmentId });
  272. if (currentSegment) {
  273. uniqueNodeIds.add(currentSegment.fromNodeId);
  274. uniqueNodeIds.add(currentSegment.toNodeId);
  275. }
  276. }
  277.  
  278. // Shift unique nodes
  279. for (const nodeId of uniqueNodeIds) {
  280. const node = sdk.DataModel.Nodes.getById({ nodeId });
  281. if (node) {
  282. let newNodeGeometry = structuredClone(node.geometry);
  283. const nodeBearing = numOffset > 0 ? DIRECTION.NORTH : DIRECTION.SOUTH;
  284. const nodeDistance = Math.abs(numOffset);
  285. const currentNodePoint = node.geometry.coordinates;
  286. const newNodePoint = turf.destination(currentNodePoint, nodeDistance, nodeBearing, { units: 'meters' });
  287. newNodeGeometry.coordinates = newNodePoint.geometry.coordinates;
  288. sdk.DataModel.Nodes.moveNode({ id: node.id, geometry: newNodeGeometry });
  289. }
  290. }
  291. }
  292.  
  293. // Update Segment Geometries
  294. for (const segmentId of selectedSegmentIds) {
  295. const currentSegment = sdk.DataModel.Segments.getById({ segmentId });
  296. if (currentSegment) {
  297. let newGeometry = structuredClone(currentSegment.geometry);
  298. const originalLength = currentSegment.geometry.coordinates.length;
  299. const shiftDistance = Math.abs(numOffset);
  300. const shiftBearing = numOffset > 0 ? DIRECTION.NORTH : DIRECTION.SOUTH;
  301.  
  302. if (disconnectNodes) {
  303. // Shift all points including end nodes
  304. for (let j = 0; j < originalLength; j++) {
  305. const currentPoint = currentSegment.geometry.coordinates[j];
  306. const newPoint = turf.destination(currentPoint, shiftDistance, shiftBearing, { units: 'meters' });
  307. newGeometry.coordinates[j] = newPoint.geometry.coordinates;
  308. }
  309. } else {
  310. // Shift only inner points
  311. for (let j = 1; j < originalLength - 1; j++) {
  312. const currentPoint = currentSegment.geometry.coordinates[j];
  313. const newPoint = turf.destination(currentPoint, shiftDistance, shiftBearing, { units: 'meters' });
  314. newGeometry.coordinates[j] = newPoint.geometry.coordinates;
  315. }
  316. // Update end points to match (potentially) moved nodes
  317. const fromNodeAfterMove = sdk.DataModel.Nodes.getById({ nodeId: currentSegment.fromNodeId });
  318. const toNodeAfterMove = sdk.DataModel.Nodes.getById({ nodeId: currentSegment.toNodeId });
  319.  
  320. if (fromNodeAfterMove && newGeometry.coordinates.length > 0) {
  321. newGeometry.coordinates[0] = fromNodeAfterMove.geometry.coordinates;
  322. }
  323. if (toNodeAfterMove && newGeometry.coordinates.length > 1) {
  324. newGeometry.coordinates[originalLength - 1] = toNodeAfterMove.geometry.coordinates;
  325. }
  326. }
  327. sdk.DataModel.Segments.updateSegment({ segmentId: currentSegment.id, geometry: newGeometry });
  328. }
  329. }
  330. }, 'Shifted segments vertically');
  331. }
  332.  
  333. function ShiftSegmentNodesLon(offset) {
  334. const selectedSegmentIds = sdk.Editing.getSelection()?.ids;
  335. if (!selectedSegmentIds || selectedSegmentIds.length === 0) {
  336. return;
  337. }
  338.  
  339. const numOffset = parseFloat(offset);
  340. if (isNaN(numOffset)) {
  341. console.error('SS UTIL: Invalid shift amount for Longitude.');
  342. return;
  343. }
  344.  
  345. const disconnectNodes = $('#chkDisconnectNodes').is(':checked'); // Checkbox state
  346.  
  347. sdk.Editing.doActions(() => {
  348. const uniqueNodeIds = new Set();
  349.  
  350. if (!disconnectNodes) {
  351. for (const segmentId of selectedSegmentIds) {
  352. const currentSegment = sdk.DataModel.Segments.getById({ segmentId });
  353. if (currentSegment) {
  354. uniqueNodeIds.add(currentSegment.fromNodeId);
  355. uniqueNodeIds.add(currentSegment.toNodeId);
  356. }
  357. }
  358.  
  359. for (const nodeId of uniqueNodeIds) {
  360. const node = sdk.DataModel.Nodes.getById({ nodeId });
  361. if (node) {
  362. let newNodeGeometry = structuredClone(node.geometry);
  363. const nodeBearing = numOffset > 0 ? DIRECTION.EAST : DIRECTION.WEST;
  364. const nodeDistance = Math.abs(numOffset);
  365. const currentNodePoint = node.geometry.coordinates;
  366. const newNodePoint = turf.destination(currentNodePoint, nodeDistance, nodeBearing, { units: 'meters' });
  367. newNodeGeometry.coordinates = newNodePoint.geometry.coordinates;
  368. sdk.DataModel.Nodes.moveNode({ id: node.id, geometry: newNodeGeometry });
  369. }
  370. }
  371. }
  372.  
  373. for (const segmentId of selectedSegmentIds) {
  374. const currentSegment = sdk.DataModel.Segments.getById({ segmentId });
  375. if (currentSegment) {
  376. let newGeometry = structuredClone(currentSegment.geometry);
  377. const originalLength = currentSegment.geometry.coordinates.length;
  378. const shiftDistance = Math.abs(numOffset);
  379. const shiftBearing = numOffset > 0 ? DIRECTION.EAST : DIRECTION.WEST;
  380.  
  381. if (disconnectNodes) {
  382. for (let j = 0; j < originalLength; j++) {
  383. const currentPoint = currentSegment.geometry.coordinates[j];
  384. const newPoint = turf.destination(currentPoint, shiftDistance, shiftBearing, { units: 'meters' });
  385. newGeometry.coordinates[j] = newPoint.geometry.coordinates;
  386. }
  387. } else {
  388. for (let j = 1; j < originalLength - 1; j++) {
  389. const currentPoint = currentSegment.geometry.coordinates[j];
  390. const newPoint = turf.destination(currentPoint, shiftDistance, shiftBearing, { units: 'meters' });
  391. newGeometry.coordinates[j] = newPoint.geometry.coordinates;
  392. }
  393. const fromNodeAfterMove = sdk.DataModel.Nodes.getById({ nodeId: currentSegment.fromNodeId });
  394. const toNodeAfterMove = sdk.DataModel.Nodes.getById({ nodeId: currentSegment.toNodeId });
  395.  
  396. if (fromNodeAfterMove && newGeometry.coordinates.length > 0) {
  397. newGeometry.coordinates[0] = fromNodeAfterMove.geometry.coordinates;
  398. }
  399. if (toNodeAfterMove && newGeometry.coordinates.length > 1) {
  400. newGeometry.coordinates[originalLength - 1] = toNodeAfterMove.geometry.coordinates;
  401. }
  402. }
  403. sdk.DataModel.Segments.updateSegment({ segmentId: currentSegment.id, geometry: newGeometry });
  404. }
  405. }
  406. }, 'Shifted segments horizontally');
  407. }
  408.  
  409. //Left
  410. function SSShiftLeftClick(e) {
  411. e.stopPropagation();
  412. ShiftSegmentNodesLon(-parseFloat($('#shiftAmount').val())); // Negative for West
  413. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the left.`, false, false, 1500);
  414. }
  415. //Right
  416. function SSShiftRightClick(e) {
  417. e.stopPropagation();
  418. ShiftSegmentNodesLon(parseFloat($('#shiftAmount').val())); // Positive for East
  419. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the right.`, false, false, 1500);
  420. }
  421. //Up
  422. function SSShiftUpClick(e) {
  423. e.stopPropagation();
  424. ShiftSegmentNodesLat(parseFloat($('#shiftAmount').val()));
  425. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the up.`, false, false, 1500);
  426. }
  427. //Down
  428. function SSShiftDownClick(e) {
  429. e.stopPropagation();
  430. ShiftSegmentNodesLat(-parseFloat($('#shiftAmount').val()));
  431. WazeWrap.Alerts.info('WME Segment Shift Utility', `The segments are shifted by <b>${$('#shiftAmount').val()} Metres</b> to the down.`, false, false, 1500);
  432. }
  433.  
  434. function injectCss() {
  435. const css = [].join(' '); // No custom CSS needed if these were the only ones
  436. $('<style type="text/css">' + css + '</style>').appendTo('head');
  437. }
  438. /*
  439. Changelog
  440. 2025.07.04.01
  441. - Added keyboard shortcuts (Alt + Arrow keys) for quick segment shifting in all four directions. This improves workflow speed and matches the behavior of other WME utility scripts.
  442. */
  443. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址